9

我陷入了经典设计的困境。我正在编写一个包含值和测量单位元组(例如 7.0 毫米)的 C# 数据结构,我想知道是否应该使用引用类型或值类型。

结构的好处应该是更少的堆操作,让我在表达式中获得更好的性能,并减少垃圾收集器的压力。对于像这样的简单类型,这通常是我的选择,但在这种具体情况下存在缺点。

元组是相当通用的分析结果框架的一部分,其中结果在 WPF 应用程序中以不同的方式呈现,具体取决于结果值的类型。WPF 使用它的所有数据模板、值转换和模板选择器处理这种弱类型非常好。这意味着如果我的元组表示为结构,则该值将经历大量装箱/拆箱。事实上,元组在表达式中的使用对于装箱场景中的使用来说是次要的。为了避免所有的拳击,我考虑将我的类型声明为一个类。另一个对结构体的担忧是,WPF 中的双向绑定可能存在缺陷,因为在代码中的某处得到元组的副本而不是引用副本会更容易。

我也有一些方便的运算符重载。我可以使用重载的比较运算符将毫米与厘米进行比较,而不会出现问题。但是,如果我的元组是一个类,我不喜欢重载 == 和 != 的想法,因为约定是 == 和 != 是引用类型的 ReferenceEquals(与 System.String 不同,这是另一个经典讨论)。如果 == 和 != 被重载,有人会写if (myValue == null)并在有一天 myValue 为 null 时得到一个讨厌的运行时异常。

另一个方面是,在 C# 中(与例如 C++ 不同)没有明确的方法来区分代码使用中的引用类型和值类型,但语义却非常不同。我担心我的元组(如果声明为结构)的用户假定类型是一个类,因为大多数自定义数据结构都是并且假定引用语义。这是另一个论点,为什么人们应该更喜欢类,因为那是用户所期望的并且没有“。” /“->”来区分它们。一般来说,除非我的分析器告诉我使用结构,否则我几乎总是使用类,这仅仅是因为类语义最有可能被其他程序员所期望,而 C# 只是模糊地暗示它是一件事还是另一件事。

所以我的问题是:

在决定是否应该进行价值或参考时,我应该考虑哪些其他因素?

== / != 在任何情况下都可以证明类中的重载是合理的吗?

程序员假设东西。大多数人可能会认为所谓的“点”是一种值类型。如果您阅读一些带有“UnitValue”的代码,您会假设什么?

鉴于我的使用说明,您会选择什么?

4

6 回答 6

9

结构的好处应该是更少的堆操作,让我在表达式中获得更好的性能,并减少垃圾收集器的压力

在没有任何上下文的情况下,这是一个巨大且危险的过度概括。结构不会自动符合堆栈的条件。如果(且仅当)结构的生命周期和暴露没有扩展到声明它的函数之外,它没有被装箱在该函数中,并且可能还有许多其他标准不被装箱,则可以将结构放在堆栈上立即浮现在脑海中。这意味着使其成为 lambda 表达式或委托的一部分意味着无论如何它都将存储在堆上。关键是不要担心它,因为你的瓶颈有 99.9% 的可能性在其他地方

至于运算符重载,没有什么能阻止您(从技术上或哲学上)在您的类型上重载运算符。虽然您在技术上是正确的,默认情况下,引用类型之间的相等比较在语义上等同于object.ReferenceEquals,但这不是一个万能的规则。关于运算符重载,有两点需要牢记:

1.)(从实际角度来看,这可能是最重要的)运算符不是多态的。也就是说,您将只使用在类型上定义的运算符,因为它们被引用,而不是它们实际存在。

例如,如果我声明一个类型,该类型Foo定义了一个总是返回的重载 equals 运算符true,那么我这样做:

Foo foo1 = new Foo();
Foo foo2 = new Foo();
object obj1 = foo1;

bool compare1 = foo1 == foo2; // true
bool compare2 = foo1 == obj1; // false

尽管obj1实际上是 的一个实例Foo,但在我引用存储在引用中的实例的类型层次结构级别上不存在重载运算符obj1,因此它回退到引用比较。

2.) 比较操作应该是确定性的。应该不可能使用重载运算符比较相同的两个实例并且能够产生不同的结果。实际上,这种要求通常会导致类型是不可变的(因为能够区分一个类中的一个或多个值true但从等号运算符中获取是相当违反直觉的),但从根本上说,它只是意味着你不应该能够改变实例中的状态值,从而改变比较操作的结果。如果在你的场景中能够改变一些是有意义的实例状态信息而不影响比较结果,那么没有理由不应该这样做。这只是一个罕见的案例。

于 2010-10-06T13:33:49.507 回答
1

但是,如果我的元组是一个类,我不喜欢重载 == 和 != 的想法,因为约定是 == 和 != 是引用类型的 ReferenceEquals

不,约定略有不同:

(与 System.String 不同,这是另一个经典讨论)。

不,这是相同的讨论。

关键不在于类型是否是引用类型。– 类型是否表现为值。这对于 是正确的,并且对于您希望重载和String的任何类都应该是正确的。operator ==!=

在设计逻辑上为值的类型时,您应该注意一件事:使其不可变(请参阅 Stack Overflow 上的其他讨论),并正确实现比较语义:

如果 == 和 != 被重载,有人会写 if (myValue == null) 并在有一天 myValue 为 null 时得到一个讨厌的运行时异常。

应该没有异常(毕竟(string)null == null也不会产生异常!),这将是重载运算符实现中的一个错误。

于 2010-10-06T13:18:39.140 回答
0

也许您可以从Eric Lippert最近的这篇博文中获得一些灵感。使用结构时要记住的最重要的事情是使它们不可变。这是 Jon Skeet 的一篇有趣的博客文章,其中可变结构可能导致非常难以调试的问题。

于 2010-10-06T13:18:57.643 回答
0

我不确定在 UI 代码中对您的值进行装箱/拆箱的性能损失应该是您主要关心的问题。例如,与布局过程相比,这种性能命中将是次要的。

事实上,你可以用另一种方式表达你的问题:你希望你的类型是可变的还是不可变的?我认为不变性与你的规格是合乎逻辑的。它是一个值,你自己说过,将它命名为 UnitValue。作为开发人员,我会很惊讶 UnitValue 不是一个值 ;) => 使用不可变结构

此外,null 对测量没有任何意义。平等和比较也应该遵循测量规则来实施。

不,我看不出在您的情况下使用 ref 类型而不是值类型的相关理由。

于 2010-10-06T13:28:21.990 回答
0

在我看来,您的设计需要元组的值类型语义。从程序员的角度来看,<7.0, mm> 应该始终等于 <7.0, mm>。<7.0, mm> 正好是各部分的总和,没有自己的身份。其他一切我都会觉得很混乱。这种 if 也意味着不变性。

现在,如果你用结构或类来实现它取决于性能,以及你是否必须支持每个元组的空值。如果您选择结构,如果您只需要在少数情况下支持 null ,则可以使用 Nullable 。

另外,你不能为你的元组提供一个引用类型包装器,用于显示目的吗?我不熟悉 WPF,但我想这将消除所有的装箱操作。

于 2010-10-06T13:34:18.160 回答
0

用于包含值和测量单位元组的数据结构(例如 7.0 毫米)

听起来它具有价值语义。该框架提供了一种创建具有值语义的类型的机制,即struct. 用那个。

您在问题的下一段中所说的几乎所有内容,赞成和反对价值类型都是基于它将如何与运行时的实现细节进行交互来优化的问题。由于在这方面有利有弊,因此没有明显的效率赢家。既然没有实际尝试就无法找到明确的效率赢家,因此任何在这方面进行优化的尝试显然都为时过早。尽管我已经厌倦了那句关于过早优化的说法,因为有人试图让某些东西变得更快或更小,但它确实适用于此。

有一件事与优化无关:

如果我的元组是一个类,我不喜欢重载 == 和 != 的想法,因为约定是 == 和 != 是引用类型的 ReferenceEquals

一点也不真实。默认值是 == 和 != 处理引用相等性,但这也是因为它是唯一有意义的默认值,而无需更多地了解类的语义。== 和 != 当它符合类语义时应该重载,当引用相等是唯一关心的事情时,应该使用 ReferenceEquals。

如果 == 和 != 被重载,有人会写 if (myValue == null) 并在有一天 myValue 为 null 时得到一个讨厌的运行时异常。

仅当 == 重载有新手错误时。正常的方法是:

public static bool operator == (MyType x, MyType y)
{
  if(ReferenceEquals(x, null))
    return ReferenceEquls(y, null);
  if(ReferenceEquals(y, null))
    return false;
  return x.Equals(y);
}

当然,Equals 重载还应该检查参数是否为 null,如果是则返回 false,以便人们直接调用它。当一个或两个值为 null 时,通过默认 == 行为调用它甚至不会对性能产生重大影响,那么有什么问题呢?

另一个方面是,在 C# 中(与例如 C++ 不同)没有明确的方法来区分代码使用中的引用类型和值类型,但语义却非常不同。

并不真地。就相等而言,默认语义非常不同,但是由于您将某些东西描述为打算具有值语义,因此倾向于将其作为值类型,而不是作为类类型。除此之外,可用的语义大致相同。这些机制在装箱、参考共享等方面可能有所不同,但这又回到了优化。

== / != 在任何情况下都可以证明类中的重载是合理的吗?

我宁愿问,重载 == 和 != 是合理的吗?

至于我作为程序员对“UnitValue”的假设,我可能会假设它是一个结构,因为它听起来应该是。但实际上,我什至不会假设,因为我大多不在乎,直到我在重要的地方对它做一些事情,考虑到它听起来也应该是不可变的,所以它是一个缩减集(可变之间的语义差异引用类型和可变结构在实践中更大,但这是不可变的)。

于 2010-10-06T13:42:32.183 回答