【问题标题】:Why are System.Windows.Point & System.Windows.Vector mutable?为什么 System.Windows.Point & System.Windows.Vector 是可变的?
【发布时间】:2012-01-19 01:47:23
【问题描述】:

鉴于可变结构通常被认为是邪恶的(例如,Why are mutable structs “evil”?),是否存在可能促使 .NET 框架的设计者使 System.Windows.PointSystem.Windows.Vector 可变的潜在好处?

我想了解这一点,以便我可以决定让我自己的类似结构可变(如果有的话)是否有意义。将PointVector 设为可变的决定可能只是判断上的错误,但如果有充分的理由(例如,性能优势),我想了解它是什么。

我知道我曾多次偶然发现Vector.Normalize() 方法的实现,因为令人惊讶的是(!)它不会返回新的Vector。它只是改变了当前向量。

我一直认为它应该这样工作:

var vector = new Vector(7, 11);
var normalizedVector = vector.Normalize(); // Bzzz! Won't compile

但它实际上是这样工作的:

var vector = new Vector(7, 11);
vector.Normalize(); // This compiles, but now I've overwritten my original vector

...所以,为了避免混淆,似乎不变性是一个好主意,但同样,在某些情况下,这种潜在的混淆可能是值得的。

【问题讨论】:

  • 这个问题似乎跑题了,因为没有人能回答为什么 .NET 的设计者会做出决定(除非安德斯或其中一位原始设计者碰巧看到了这个问题)。因此,无法明确回答,只是打开一个讨论话题。投票将其迁移到programmers
  • .NET 设计者从不认为结构是邪恶的,所以他们没有理由这样做。当人们普遍认为程序员难以理解值和引用类型之间的区别时,这整个邪恶的东西才出现。如果我们跟随您的领导,接下来将取缔 i++。
  • 只要发出 Eric Lippert 信号就行了。
  • @Ken,我认为有很多人可能会冒险合理地猜测为什么设计是这样的(例如,stackoverflow.com/tags/c%23/topusers)。我不需要真正的 .NET 架构师来获得合理的答案,我只是想了解使这些特定结构可变的潜在好处。虽然我不知道答案,但我觉得这个问题并不是那么主观。
  • 这是一个很好的论点,但恐怕我还是不能同意。我的意思是,除非最初的决策者之一回答(并且可以证明是那些可以知道的人之一),否则这只是表达的意见(正如你所说,一个“合理的猜测”),因此没有唯一的答案可以明确地被选为正确的。这使它成为一个讨论而不是一个问题/答案,根据FAQ,它在这里是题外话。 (请注意,我没有否决这个问题;我只是投票支持迁移它。)

标签: c# .net struct immutability mutable


【解决方案1】:

这些类型位于System.Windows 命名空间中,通常用于 WPF 应用程序。应用程序的 XAML 标记是框架的重要组成部分,因此对于很多事情,它们需要一种使用 XAML 表达的方式。不幸的是,没有办法使用 WPF XAML 调用非无参数构造函数(但在松散的 XAML 中是可能的),因此尝试使用适当的参数调用构造函数来初始化它是不可能的。您只能设置对象属性的值,因此很自然,这些属性需要是可变的。

这是一件坏事吗?对于这些类型,我会说不。它们只是用于保存数据,仅此而已。如果您想获得Window 想要的大小,您可以访问DesiredSize 来获得代表所需大小的Size 对象。您并不是要通过更改您获得的Size 对象的WidthHeight 属性来“更改所需的大小”,而是通过提供一个新的Size 对象来更改大小。我相信这样看更自然。

如果这些对象更复杂并且执行更复杂的操作或具有状态,那么是的,您不会想让这些类型既不可变也不可变结构。然而,由于它们尽可能简单和基本(本质上是一个 POD),因此在这里使用结构体是合适的。

【讨论】:

  • 非常有趣的 +1...我没想到 XAML 参数。
  • 那么为什么首先要让它们成为结构呢?是否像避免额外的间接层对性能造成影响一样简单?
  • @SeanU:我不知道我是否可以对此给出满意的答案,但话说回来,为什么要让他们上课?他们只是保存数据,并没有做任何特别的事情,没有真正需要让他们成为 AFAIK 类。
  • @JeffMercado FWIW,我只是将一个微基准放在一起,它创建了一个向量数组,然后对它们进行规范化。 class 版本的运行时间大约是 struct 版本的两倍。
  • @SeanU 在类似的行中,我将可变结构的用途更改为可变类,并且单元测试的总时间(即包括那些未触及它们的测试)变为大约 12%-慢 13%。足以让我相信我做出了正确的决定,尽管我确实将结构设置为内部结构,因此不太可能以他们的行为方式咬人。
【解决方案2】:

此类类型是可变的,因为与某些人可能声称的相反,可变值类型语义很有用。有几个地方 .net 试图假装值类型应该与引用类型具有相同的语义。由于可变值类型语义与可变引用类型语义根本不同,因此假装它们相同会导致问题。然而,这并没有使它们变得“邪恶”——它只是显示了对象模型中的一个缺陷,该模型假设对某物的副本进行操作在语义上等同于对原始文件进行操作。如果所讨论的事物是对象引用,则为真;如果它是一个不可变的结构,通常是正确的——但也有例外;如果是可变结构,则为 false。

具有暴露字段的结构的优点之一是它们的语义很容易通过简单的检查来确定。如果一个有Point[100] PointArray,则一个有100 个不同的Point 实例。如果有人说PointArray[4].X = 9;,那将更改PointArray 中的一项,而不会更改其他项。

假设不使用结构Point,而是有一个可变类PointClass

类 PointClass {public int X;公共整数 Y;};

PointClass[100] PointClassArray 中存储了多少个 PointClass 实例?有没有办法告诉?语句PointClass[4].X = 9 会影响PointClass[2].X 的值吗? someOtherObject.somePoint.X呢?

虽然 .net 集合不太适合存储可变结构,但我仍然认为:

字典; ... 点 temp = myDict["George"]; 温度 X = 9; myDict["乔治"] = temp;

具有相对清晰的语义,至少在没有线程问题的情况下。虽然我认为不幸的是 .net 集合没有提供一种可以简单地说 myDict[lookupKey].X = 9; 的方法,但我仍然认为上面的代码非常清晰且不言自明,而无需了解 任何事情关于 Point 除了它有一个名为 X 的公共整数字段这一事实之外。相比之下,如果一个人有一个 Dictionary<PointClass>,则不清楚应该做什么来更改与“George”相关的 X 值。也许与 George 关联的 PointClass 实例没有在其他任何地方使用,在这种情况下,可以简单地编写适当的字段。另一方面,也有可能其他人抓取了MyDict["George"] 的副本以捕获其中的值,并且没有预料到他抓取的PointClass 对象可能会发生变化。

有些人可能认为“Point”应该是一个不可变的结构,但是像somePoint.X = 5; 这样的语句的效果可以完全确定,只知道somePoint 是一个Point 类型的变量,而这又是一个具有名为 X 的公共 int 字段的结构。如果Point 是一个不可变结构,则必须改为使用somePoint = new Point(5, somePoint.Y); 之类的内容,除了速度较慢之外,还需要检查结构以确定其所有字段都在构造函数中初始化,其中 X 为第一个和第二个。这在什么意义上比somePoint.X = 5; 有所改进?

顺便说一句,可变结构的最大“陷阱”源于这样一个事实,即系统无法区分改变“this”的结构方法和不改变“this”的结构方法。一大耻辱。首选的解决方法是使用返回从旧结构派生的新结构的函数,或者使用接受“ref”结构参数的静态函数。

【讨论】:

  • +1 是一个发人深省的答案,但我有几个 cmets/问题:关于您关于 Point 类实例数组的示例,我为什么真的想知道有多少唯一实例这样的阵列?至于您的论点,即如果结构是不可变的,则很难设置其中一个字段值,我可以反驳说,该结构只需要一对public Point ReplaceX(double x) { return new Point(x, this.Y); } 形式的便利方法。用法为ImmutablePoints[5].ReplaceX(5);
  • 取消我的第一个问题,我重读了那段,现在我明白了你的意思。在更改类实例的属性时,您真正想知道的是,是否有其他人将同一个实例用于其他目的。不过,如果这是个问题,也可以将类设为不可变。
  • @DanM:如果两个 Point 实例不同,这并不意味着 values 不同——只是实例可以相互独立地修改。可以使用您描述的形式的便捷方法(尽管用法是ImmutablePoints[5]=ImmutablePoints[5].ReplaceX(5)),但是代码会变慢,并且读写代码所需的信息量会增加。为确保上述语句实际上按预期运行,必须检查ReplaceX 的定义以及构造函数的定义,并且...
  • ...*all* 结构的支持字段。如果结构只有两个字段,那还不错,但即使使用四字段结构(例如Rectangle),该模式也会变得烦人并且容易出错,特别是如果有任何构造函数变体将某些字段保留为默认值价值观。如果目标是拥有类似于另一个对象的东西,除了一个字段不同,那么复制对象并更改字段以将所有字段手动复制到新对象并希望这样做正确。
  • @DanM:不可变类和不可变结构具有非常相似的语义。主要区别在于类不能有任何不可为空的默认值,在大多数位置都包含对共享实例的引用的情况下,类比结构更有效,在大多数实例不同的情况下,结构通常比类更有效.
【解决方案3】:

可能性:

  1. 对于那些不考虑它会咬人的用例的人来说,这在当时似乎是个好主意。 List<T>.Enumerator 是一个可变结构,因为在当时利用经常发生的微选项似乎是个好主意。这几乎是可变结构体“邪恶”的典型代表,因为它被多个人咬伤。不过,在当时的某些人看来,这似乎是个好主意……
  2. 他们确实想到了缺点,但他们知道一些用例,其中性能差异有利于结构(并非总是如此)并且被认为很重要。
  3. 他们不认为结构是邪恶的。 “邪恶”是一种关于消极面击败积极面的观点,不是一个可证明的事实,即使 Eric Lippert 和 Jon Skeet 这么说,也不是每个人都必须同意某件事。我个人认为他们不是邪恶的,他们只是被误解了;但话又说回来,对于程序员来说,邪恶通常比被误解更容易处理,所以实际上情况更糟...... ;) 也许相关的人不同意。

【讨论】:

  • 值得注意的是,List.Enumerator 只有在使用受限泛型或类型推断时才会真正引起问题。将 List.Enumerable 转换为 IEnumerator 会将其转换为引用类型。只有当它实际用作List.Enumerator 类型时,它才会表现为值类型;在没有泛型或类型推断的情况下,以这种方式声明它的唯一原因是如果有人想利用它的值语义。我怀疑List<T>.Enumerator 追随List.Enumerator 的脚步,因为否则会违反...
  • ...对于习惯使用List.Enumerator的人来说,最小惊讶原则。
  • 那个和GetEnumerator 在可见性方面适合一个有趣的空间。它在形式上与任何其他成员一样可见,但通常不直接调用。在分析“我需要多少担心呼叫者做一些疯狂的事情”时,它介于私人和大多数公共用途之间。关于这一点我还有很多话要说,所以我想我可能会为“可变类型是邪恶的”线程添加一个答案,这个线程已经很老了,但不断出现。
  • 这与我提出的一个问题有些联系,也许不是很好,想知道是否有任何方法可以以声明方式阻止外部代码持久化类型来定义类型。例如,可以定义一个PointList,其索引属性将返回一个结构,其中包含对PointList 和下标的引用的私有字段。这可以反过来定义“X”和“Y”属性,这些属性将使用GetXAt()SetYAt() 等方法来允许MyList[4].X = 9 等构造。
  • @supercat 也许最好的方法是用 C# 以外的语言编写它,这将允许返回和使用托管引用,尽管它无法验证.由于您不能拥有托管引用的字段,也不能将它们序列化,因此会限制持久性。
猜你喜欢
  • 1970-01-01
  • 2011-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-08
  • 2010-10-01
  • 1970-01-01
相关资源
最近更新 更多