在关于 SO 的讨论之后,我已经多次阅读了可变结构是“邪恶的”的评论(就像在这个问题的答案中一样) 问题).

C# 中的可变性和结构的实际问题是什么?

有帮助吗?

解决方案

结构是值类型,这意味着当它们被四处传递它们被复制。

所以,如果你改变一个副本只是更改了该副本,而不是原来的,而不是任何其他副本,这可能是各地。

如果你的结构是不可改变的然后从由值被传递所产生的所有自动拷贝将是相同的。

如果你想改变它,你必须自觉与修改后的数据创建结构的新实例做到这一点。 (不是复制)

其他提示

从哪里开始;-p

埃里克利珀的博客是总是为报价好:

  

这是另一个原因可变   值类型是邪恶的。尝试总是   使值类型不可变的。

首先,你往往会失去改变很容易...例如,把事情出了名单:

Foo foo = list[0];
foo.Name = "abc";

什么这样做的改变?任何有用...

用特性相同的:

myObj.SomeProperty.Size = 22; // the compiler spots this one

强迫你做的:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

少重要的是,有一个大小问题;可变对象的倾向于以具有多个属性;但如果你有两个ints,一个string,一个DateTimebool一个结构,可以非常快速地通过大量的内存燃烧。一类中,多个呼叫者可以共享对相同实例的引用(参考文献是小的)。

我不会说的的但可变性往往是急急忙忙的对程序员的部分标志提供最大的功能。在现实中,这通常是不需要的,并且,反过来,使接口更小,更容易使用,更难使用错误(=更鲁棒)。

这样的一个例子是读/写,写/写竞争条件冲突。这些根本无法发生在不可变的结构中,由于写操作是无效操作。

另外,我声称的可变性是几乎从来没有实际需要 ,程序员只的认为的,它的可能的是未来。例如,它根本没有任何意义来更改日期。相反,创建基于关闭旧的一个新的日期。这是一种廉价的操作,因此性能不是一个考虑因素。

可变的结构是不恶。

他们是在高性能的情况下绝对必要的。例如,当高速缓存行和或垃圾收集成为瓶颈。

我不会把使用一成不变的结构,在这些完全有效的使用情况“邪恶”。

我可以看到,C#的语法不帮助区分值类型或引用类型的一个成员的接入点,所以我所有的宁愿不可变结构,即执行不变性,用可变的结构。

不过,而不是简单地标记不变结构为“邪恶”的,我会建议接受的语言和提倡的大拇指更加有益和建设性的规则。

例如:“结构是值类型,默认情况下复制你需要一个参考,如果你不希望复制他们” 的或 “尝试首先用只读结构工作”

具有公共可变字段或属性的结构并不是邪恶的。

改变“this”的结构方法(与属性设置器不同)有些邪恶,只是因为 .net 没有提供区分它们和不改变“this”的方法的方法。即使在只读结构上,也应该可以调用不改变“this”的结构方法,而无需进行防御性复制。改变“this”的方法根本不应该在只读结构上调用。由于 .net 不想禁止在只读结构上调用不修改“this”的结构方法,但又不想允许只读结构发生变异,因此它防御性地以只读方式复制结构只有上下文,可以说是两全其美。

然而,尽管在只读上下文中处理自变异方法存在问题,但可变结构通常提供远远优于可变类类型的语义。考虑以下三个方法签名:

struct PointyStruct {public int x,y,z;};
class PointyClass {public int x,y,z;};

void Method1(PointyStruct foo);
void Method2(ref PointyStruct foo);
void Method3(PointyClass foo);

对于每种方法,回答以下问题:

  1. 假设该方法不使用任何“不安全”代码,它可能会修改 foo 吗?
  2. 如果在调用该方法之前不存在对“foo”的外部引用,那么在调用该方法之后是否可能存在外部引用?

答案:

问题一:
Method1(): :不 (意图明确)
Method2(): :是的 (意图明确)
Method3(): :是的 (不确定意图)
问题2:
Method1(): :不
Method2(): :不 (除非不安全)
Method3(): :是的

Method1 无法修改 foo,并且永远不会获得引用。Method2 获取对 foo 的短暂引用,它可以使用该引用以任何顺序修改 foo 的字段任意次数,直到它返回,但它无法保留该引用。在 Method2 返回之前,除非它使用不安全的代码,否则可能由其“foo”引用构成的任何和所有副本都将消失。与 Method2 不同,Method3 获取对 foo 的混杂共享引用,并且不知道它会用它做什么。它可能根本不会改变 foo,它可能会改变 foo 然后返回,或者它可能会将 foo 的引用提供给另一个线程,该线程可能会在未来的某个任意时间以某种任意方式改变它。限制 Method3 对传递给它的可变类对象执行的操作的唯一方法是将可变对象封装到只读包装器中,这是丑陋且麻烦的。

结构数组提供了美妙的语义。给定 Rectangle 类型的 RectArray[500],如何显而易见,例如将元素 123 复制到元素 456,然后一段时间后将元素 123 的宽度设置为 555,而不会干扰元素 456。“矩形数组[432] = 矩形数组[321];...;矩形数组[123].宽度= 555;”。知道 Rectangle 是一个具有名为 Width 的整数字段的结构体,就可以了解上述语句所需的所有信息。

现在假设 RectClass 是一个与 Rectangle 具有相同字段的类,并且想要对 RectClass 类型的 RectClassArray[500] 执行相同的操作。也许该数组应该保存 500 个对可变 RectClass 对象的预先初始化的不可变引用。在这种情况下,正确的代码将类似于“RectClassArray[321].SetBounds(RectClassArray[456]);...;RectClassArray[321].X = 555;”。也许数组被假定保存不会更改的实例,因此正确的代码更像是“RectClassArray[321] = RectClassArray[456];...;矩形数组[321] = 新矩形(矩形数组[321]);RectClassArray[321].X = 555;" 要知道一个人应该做什么,就必须了解更多关于 RectClass 的知识(例如它是否支持复制构造函数、复制方法等)以及数组的预期用途。远没有使用结构那么干净。

可以肯定的是,不幸的是,除了数组之外,没有任何容器类可以提供结构体数组的干净语义。最好的方法是,如果想要一个集合被索引,例如一个字符串,可能会提供一个通用的“ActOnItem”方法,该方法将接受索引字符串、通用参数和委托,该委托将通过引用传递通用参数和集合项。这将允许与结构数组几乎相同的语义,但除非可以说服 vb.net 和 C# 人员提供良好的语法,否则即使性能合理(传递通用参数将允许使用静态委托并避免创建任何临时类实例的需要)。

就我个人而言,我对埃里克·利珀特等人的仇恨感到恼火。关于可变值类型的喷涌。它们提供比到处使用的混杂引用类型更清晰的语义。尽管 .net 对值类型的支持存在一些限制,但在许多情况下,可变值类型比任何其他类型的实体都更适合。

值类型基本上代表不可变的概念。 Fx的,它是没有意义的具有数学值,诸如整数,矢量等,然后可以对其进行修改。这就好比重新定义价值的含义。而不是改变一个值类型的,它更有意义指定另一个独特的价值。想想一个事实,即值类型比较其属性的所有值进行比较。点是,如果属性是相同的,然后它是该值的相同的通用表示。

作为康拉德提到它是没有意义的,以要么改变的日期,作为值表示在时间点独特和不具有任何状态或上下文依赖性的时间对象的实例。

希望这使得任何意义给你。它更多的是你尝试用值类型不是实用的细节捕捉,以确保这一概念。

从程序员的角度来看,还有另外一些极端情况可能会导致不可预测的行为。这是他们几个。

  1. 不可变值类型和只读字段

// Simple mutable structure. 
// Method IncrementI mutates current state.
struct Mutable
{
    public Mutable(int i) : this() 
    {
        I = i;
    }

    public void IncrementI() { I++; }

    public int I {get; private set;}
}

// Simple class that contains Mutable structure
// as readonly field
class SomeClass 
{
    public readonly Mutable mutable = new Mutable(5);
}

// Simple class that contains Mutable structure
// as ordinary (non-readonly) field
class AnotherClass 
{
    public Mutable mutable = new Mutable(5);
}

class Program
{
    void Main()
    {
        // Case 1. Mutable readonly field
        var someClass = new SomeClass();
        someClass.mutable.IncrementI();
        // still 5, not 6, because SomeClass.mutable field is readonly
        // and compiler creates temporary copy every time when you trying to
        // access this field
        Console.WriteLine(someClass.mutable.I);

        // Case 2. Mutable ordinary field
        var anotherClass = new AnotherClass();
        anotherClass.mutable.IncrementI();

        //Prints 6, because AnotherClass.mutable field is not readonly
        Console.WriteLine(anotherClass.mutable.I);
    }
}

  1. 可变值类型和数组

假设有一个可变结构数组,并且我们正在为该数组的第一个元素调用 IncrementI 方法。您期望这次通话有什么行为?它应该更改数组的值还是仅更改其副本?

Mutable[] arrayOfMutables = new Mutable[1];
arrayOfMutables[0] = new Mutable(5);

// Now we actually accessing reference to the first element
// without making any additional copy
arrayOfMutables[0].IncrementI();

//Prints 6!!
Console.WriteLine(arrayOfMutables[0].I);

// Every array implements IList<T> interface
IList<Mutable> listOfMutables = arrayOfMutables;

// But accessing values through this interface lead
// to different behavior: IList indexer returns a copy
// instead of an managed reference
listOfMutables[0].IncrementI(); // Should change I to 7

// Nope! we still have 6, because previous line of code
// mutate a copy instead of a list value
Console.WriteLine(listOfMutables[0].I);

因此,只要您和团队的其他成员清楚地了解您在做什么,可变结构就不是邪恶的。但是,在很多极端情况下,程序行为与预期不同,这可能会导致难以产生和难以理解的微妙错误。

如果你曾经在如C / C ++语言编程的,结构是细使用作为可变的。只是REF通过他们,周围并没有什么可能出错。我觉得唯一的问题是C#编译器的限制,而且在某些情况下,我无法强迫愚蠢的事情来使用该结构的参考,而不是复制(当结构是一个C#类的一部分像)。

因此,可变的结构是不是邪恶的,C#具有的的他们的邪恶。我使用C ++所有的时间可变的结构,他们都非常方便和直观。相比之下,C#已经让我彻底放弃结构为,因为他们处理的对象的方式类成员。他们的便利已经花费了我们我们的。

假设你有1,000,000结构的阵列。表示与像bid_price,OFFER_PRICE(也许小数)东西股权各结构等等,这是通过C#/ VB创建。

设想一下,阵列在非托管堆中分配的内存块创建,这样一些其它的本机代码的线程能够同时访问阵列(可能有的高PERF的代码做数学)。

想象C#/ VB代码正在收听的价格的变化的市场进料,该代码可能具有访问该阵列的一些元素(无论哪个安全),然后修改某些价格字段(一个或多个)。

想象这是正在做几万甚至几十万次每秒。

好让我们面对事实,在这种情况下,我们真的希望这些结构是可变的,他们需要的是因为他们被一些其他原生代码,以便创建副本共享是不是要去帮助;他们需要因为这些速率使得一些120字节结构的副本是精神病,尤其是当更新实际上可能影响只是一个或两个字节。

雨果

如果你坚持什么结构旨在用于(在C#,Visual Basic 6中,帕斯卡/ Delphi的,当他们不被用作指针C ++结构类型(或类)),你会发现,这样的结构是不超过一个化合物变量。这意味着:你将它们视为填充组变量,在一个共同的域名(从基准构件一个记录变量)

我知道这会迷惑很多人深感用于OOP,但是这不是足够的理由说这样的话是天生邪恶,如果正确使用。一些结构inmutable因为它们打算(这是Python namedtuple的情况下),但它是另一范例考虑。

是:结构涉及到很多的记忆,但它不会做精确地更多的内存:

point.x = point.x + 1
相比

point = Point(point.x + 1, point.y)

在存储器消耗将至少是相同的,或甚至更多的inmutable情况下(尽管这种情况下将是暂时的,对于当前的堆栈,这取决于语言)。

但是,最后,结构结构,而不是对象。在POO,一个目标物的主要特性是它们的身份,其大部分时间不大于它的存储器地址等等。结构表示数据结构(未适当对象,所以它们不具有身份无论如何),和数据可被修改。在其它语言中,记录(而不是结构,如对于帕斯卡的情况下)是单词并保持同样的目的:只是一个数据记录变量,旨在被读从文件,修改,并倒入文件(即主要使用,并在许多语言中,你甚至可以在记录定义数据对齐,而这并不一定正确地对称为对象的情况下)。

要一个很好的例子?结构是用来方便地读取文件。 Python有这个库因为,因为它是面向对象和对结构的支持,它必须以另一种方式,这是有点难看实现它。实现结构的语言有特点...内置。尝试读取一个位图头在如帕斯卡尔或C语言进行适当的结构这将是容易(如果该结构是正确建立和对齐;在帕斯卡尔你不会使用一种基于记录的访问,但功能来读取任意的二进制数据)。因此,对于文件和直接(本地)内存访问,结构是不是对象更好。至于今天,我们已经习惯了JSON和XML,所以我们忘记了使用的二进制文件(并作为一个副作用,在使用结构)。但是,是的:它们的存在,并有目的

他们不是邪恶的。只需使用他们正确的目的。

如果您想在锤子的条款,你会希望把螺丝钉子,发现螺丝是很难在墙上大幅下挫,这将是螺丝的过错,他们将是邪恶的。

当某些东西可以变异时,它就会获得一种认同感。

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

因为 Person 是可变的,思考起来更自然 改变埃里克的立场克隆埃里克,移动克隆体,并摧毁原来的. 。两个操作都会成功改变内容 eric.position, ,但其中一个比另一个更直观。同样,向 Eric 传递(作为参考)修改他的方法会更直观。给一个方法克隆 Eric 几乎总是会令人惊讶。任何想要变异的人 Person 一定要记得索要参考资料 Person 否则他们会做错事。

如果你使类型不可变,问题就会消失;如果我不能修改 eric, ,我是否收到对我来说没有什么区别 eric 或克隆 eric. 。更一般地说,如果类型的所有可观察状态都保存在以下任一成员中,则可以安全地按值传递:

  • 不可变的
  • 参考类型
  • 安全地按值传递

如果满足这些条件,则可变值类型的行为类似于引用类型,因为浅拷贝仍然允许接收者修改原始数据。

不可变的直观性 Person 取决于你想做什么。如果 Person 仅仅代表一个 数据集 对于一个人来说,没有什么是不直观的; Person 变量真正代表抽象 价值观, ,不是对象。(在这种情况下,将其重命名为可能更合适 PersonData。) 如果 Person 实际上是在对一个人本身进行建模,即使您已经避免了认为自己正在修改原版的陷阱,不断创建和移动克隆的想法也是愚蠢的。在这种情况下,简单地制作可能会更自然 Person 引用类型(即类。)

当然,正如函数式编程告诉我们的那样, 一切 不可变(没有人可以秘密保留对 eric 并改变他),但由于这在 OOP 中不是惯用的,所以对于使用你的代码的其他人来说仍然是不直观的。

它与结构没有任何关系(也与 C# 无关),但在 Java 中,当可变对象是例如可变对象时,您可能会遇到问题。哈希映射中的键。如果您在将它们添加到地图后更改它们,并且它会更改其 哈希码, ,邪恶的事情可能会发生。

个人当我在看以下代码看起来很笨拙对我说:

data.value.set(data.value.get()+ 1);

,而不是简单地

data.value ++;或data.value = data.value + 1;

周围传递类时,你想确保该值以受控的方式修改数据封装是有用的。然而,当你有公共设置和获取做多一点的值设置为什么都传递函数,这是怎么改进了只是绕过一个公共数据结构?

当我创建一个类内部的专用结构,我创建了结构以一组变量组织成一组。我希望能够修改该结构类的范围之内,没有得到该结构的副本,并创建新实例。

对我来说这阻止了有效利用结构被用于组织公共变量,如果我想访问控制我会使用一个类。

先生有几个问题。埃里克·利珀特的例子。它旨在说明复制结构的这一点以及如果您不小心的话这可能会出现问题。看看这个例子,我认为这是不良编程习惯的结果,而不是结构或类的真正问题。

  1. 结构应该只具有公共成员,并且不需要任何封装。如果确实如此,那么它确实应该是一个类型/类。你确实不需要两个结构来表达同一件事。

  2. 如果您有包含结构的类,则可以调用类中的方法来更改成员结构。这就是我要做的一个良好的编程习惯。

正确的实现如下。

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

看起来这是编程习惯的问题,而不是结构本身的问题。结构应该是可变的,这就是想法和意图。

更改的结果瞧,其行为符合预期:

1 2 3按任何键继续。。。

有许多优点和缺点的可变数据。百万美元的缺点是别名。如果相同的值在多个地方被使用,其中一个改变它,那么它会似乎已经奇迹般地改变到使用它的其他地方。这涉及到,但不能与相同的,竞争条件。

在数百万美元的优点是模块化,有时。可变的状态可以让你躲改变从代码并不需要知道有关它的信息。

解释器的艺术进入在一些细节这些折衷,并给出一些例子。

我不相信,如果正确使用,他们是邪恶的。我不会把它放在我的生产代码,但我会为类似结构的单元测试嘲笑,在一个结构的寿命是比较小的。

使用埃里克例如,您可能希望创建埃里克的第二个实例,但做出调整,因为这是你的测试的性质(即重复,然后修改)。这不要紧,如果我们只是用ERIC2的测试脚本的其余部分,除非你使用他作为测试对比规划是什么与埃里克的第一个实例发生。

这将是用于测试或修改遗留代码浅定义特定对象(结构的点),但是通过具有一个不可变结构大多是有用的,这防止了它的用法烦人。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top