我应该使用结构还是类?
我陷入了经典的设计困境。我正在编写一个 C# 数据结构,用于包含值和测量单位元组(例如 7.0 毫米),我想知道是否应该使用引用类型或值类型。
结构的好处应该是减少堆操作,从而使我在表达式中获得更好的性能,并减轻垃圾收集器的压力。这通常是我对像这样的简单类型的选择,但在这种具体情况下存在缺点。
元组是相当通用的分析结果框架的一部分,其中根据结果值的类型,结果在 WPF 应用程序中以不同的方式呈现。 WPF 及其所有数据模板、值转换和模板选择器可以很好地处理这种弱类型。这意味着如果我的元组表示为结构体,则该值将经历大量装箱/拆箱。事实上,元组在表达式中的使用相对于拳击场景中的使用来说是次要的。为了避免所有的拳击,我考虑将我的类型声明为一个类。关于结构体的另一个担忧是,WPF 中的双向绑定可能存在缺陷,因为在代码中的某个位置获得元组的副本而不是引用副本会更容易。
我还有一些方便的运算符重载。我可以使用重载的比较运算符来比较毫米和厘米,而不会出现任何问题。但是,如果我的元组是一个类,我不喜欢重载 == 和 != 的想法,因为约定是 == 和 != 是引用类型的 ReferenceEquals(与 System.String 不同,这是另一个经典讨论)。如果 == 和 != 被重载,有人会编写 if (myValue == null) 并在有一天 myValue 变为 null 时得到一个讨厌的运行时异常。
另一方面是,C# 中没有明确的方法(与 C++ 等不同)来区分代码使用中的引用类型和值类型,但语义却非常不同。我担心我的元组(如果声明了结构)的用户假设该类型是一个类,因为大多数自定义数据结构都是并且假设引用语义。这是为什么人们应该更喜欢类的另一个论点,因为那是用户所期望的并且没有“。” /“->”来区分它们。一般来说,除非我的探查器告诉我使用结构,否则我几乎总是使用类,因为类语义是其他程序员最有可能期望的,而 C# 无论是一件事还是另一件事都只有模糊的提示。
所以我的问题是:
在决定应该价值还是参考时,我还应该考虑哪些其他因素?
在任何情况下,类中的 == / != 重载是否合理?
程序员承担一些事情。大多数人可能会认为所谓“点”的东西是一种值类型。如果您阅读一些带有“UnitValue”的代码,您会假设什么?
根据我的使用描述,您会选择什么?
I am in a classic design dilemma. I am writing a C# data structure for containing a value and measurement unit tuple (e.g. 7.0 millimeters) and I am wondering if I should use a reference type or a value type.
The benefits of a struct should be less heap action giving me better performance in expressions and less stress on the garbage collector. This would normally be my choice for a simple type like this, but there are drawbacks in this concrete case.
The tuple is part of a rather general analysis result framework where the results are presented in different ways in a WPF application depending on the type of the result value. This kind of weak typing is handled exceptionally well by WPF with all it's data templates, value converts and template selectors. The implication is that the value will undergo a lot of boxing / unboxing if my tuple is represented as a struct. In fact the use of the tuple in expressions will be minor to the use in boxing scenarios. To avoid all the boxing I consider declaring my type as a class. Another worry about a struct is that there could be pitfalls with two-way binding in WPF, since it would be easier to end up with copies of the tuples somewhere in the code rather than reference copies.
I also have some convenient operator overloading. I am able to compare say millimeters with centimeters without problems using overloaded comparison operators. However I don't like the idea of overloading == and != if my tuple is a class, since the convention is that == and != is ReferenceEquals for reference types (unlike System.String, which is another classic discussion). If == and != is overloaded, someone will write if (myValue == null) and get a nasty runtime exception when myValue one day turn out to be null.
Yet another aspect is that there is no clear way in C# (unlike in e.g. C++) to distinguish reference and value types in code usages, yet the semantics are very different. I worry that users of my tuple (if declared struct) assumes that the type is a class, since most custom data structures are and assumes reference semantics. That is another argument why one should prefer classes simply because thats what the user expects and there are no "." / "->" to tell them apart. In general I would almost always use a class unless my profiler tells me to use a struct, simply because class semantics is the most likely expected by fellow programmers and C# has only vague hints whether it is one thing or the other.
So my questions are:
What other considerations should I weigh in when deciding if I should go value or reference?
Can == / != overloading in a class be justified in any circumstances?
Programmers assume stuff. Most would probably assume that something called a "Point" is a value type. What would you assume if you read some code with a "UnitValue"?
What would you choose given my usage description?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
在没有任何上下文的情况下,这是一种巨大且危险的过度概括。结构不会自动符合堆栈的条件。当(且仅当)结构体的生命周期和暴露不会扩展到声明它的函数之外、它不会被装箱在该函数内,并且可能还有许多其他标准不满足时,才可以将结构体放置在堆栈上立即浮现在脑海中。这意味着将其作为 lambda 表达式或委托的一部分意味着它无论如何都会存储在堆上。 重点是不必担心,因为 99.9% 的可能性您的瓶颈在其他地方。
至于运算符重载,没有什么可以阻止您(无论是在技术上还是在哲学上)在您的类型上重载运算符。虽然您在技术上是正确的,默认情况下,引用类型之间的相等比较在语义上等同于
object.ReferenceEquals
,但这并不是一个万能的规则。关于运算符重载,需要记住两个基本事项:1.)(从实际角度来看,这可能是最重要的)运算符不是多态的。也就是说,您将仅使用在类型上定义的运算符因为它们被引用,而不是因为它们实际存在。
例如,如果我声明一个类型
Foo
,它定义了一个始终返回true
的重载 equals 运算符,那么我会这样做:即使
obj1
是实际上,在Foo
的实例中,重载运算符并不存在于我引用存储在obj1
引用中的实例的类型层次结构级别,因此它退回到参考比较。2.) 比较操作应该是确定性的。使用重载运算符比较相同的两个实例应该不可能产生不同的结果。 实际上,这种要求通常会导致类型不可变(因为能够区分类中一个或多个值之间的差异,但从 equals 运算符获得
true
是相当违反直觉的),但从根本上来说,它只是意味着您不应该能够更改实例中的状态值来更改比较操作的结果。如果在您的场景中能够改变一些实例状态信息而不影响比较结果是有意义的,那么您没有理由不这样做。这只是一个罕见的案例。Given without any context, this is a vast--and dangerous--overgeneralization. A struct is not automatically eligible for the stack. A struct can be placed on the stack if (and only if) its lifetime and exposure does not extend outside of the function that's declaring it, it doesn't get boxed within that function, and probably a host of other criteria that don't come to mind immediately. This means that making it part of an lambda expression or delegate means that it's going to be stored on the heap anyway. The point is not to worry about it, because there's a 99.9% chance that your bottlenecks are somewhere else.
As for operator overloading, there's nothing stopping you (either technically or philosophically) from overloading operators on your type. While you're technically correct in that equality comparisons between reference types are, by default, semantically equivalent to
object.ReferenceEquals
, this is not a be-all and end-all rule. There are two basic things to keep in mind about operator overloading:1.) (And this may be the most important from a practical perspective) Operators are not polymorphic. That is, you will only use operators defined on the types as they are referenced, not as they actually exist.
For example, if I declare a type
Foo
that defines an overloaded equals operator that always returnstrue
, then I do this:Even though
obj1
is, in reality, an instance ofFoo
, the overloaded operator doesn't exist at the type hierarchy level that I'm referencing the instance stored in theobj1
reference, so it falls back to reference comparison.2.) Comparison operations should be deterministic. It should not be possible to compare the same two instances using the overloaded operator and be able to yield differing results. Practically, this sort of requirement usually results in the types being immutable (since being able to tell the difference between one or more values in a class yet getting
true
from an equals operator is rather counterintuitive), but fundamentally it just means that you should not be able to alter a state value within an instance that will alter the result of a comparison operation. If it makes sense in your scenario to be able to mutate some of the instance state information without having it affect the result of a comparison, then there's no reason you shouldn't. That's just a rare case.不,约定略有不同:
不,这是同样的讨论。
关键不在于类型是否是引用类型。 – 类型是否表现为值。对于
String
来说是这样,对于您想要重载operator ==
和!=
的任何类也应该如此。在设计逻辑上是值的类型时,您只需要注意一件事:使其不可变(请参阅 Stack Overflow 上的其他讨论),并正确实现比较语义:
不应该有异常(毕竟,
(string)null == null
也不会产生异常!),这将是重载运算符实现中的一个错误。No, the convention is slightly different:
Nah, it’s the same discussion.
The crux is not whether a type is a reference type. – It’s whether the type behaves as a value. This is true for
String
, and this should be true for any class for which you care to overloadoperator ==
and!=
.There’s only one thing that you should take care of when designing a type that is logically a value: make it immutable (see other discussions here on Stack Overflow), and implement the comparison semantics properly:
There should be no exception (after all,
(string)null == null
doesn’t yield an exception either!), this would be a bug in the implementation of the overloaded operator.我不确定 UI 代码中的值装箱/拆箱的性能损失应该是您在这里主要关心的问题。例如,与布局过程相比,这种性能影响很小。
事实上,您可以用另一种方式表达您的问题:您希望您的类型是可变的还是不可变的?我认为不变性符合你的规范。这是一个值,您自己说过,将其命名为 UnitValue。作为一名开发人员,我会很惊讶 UnitValue 不是一个值 ;) =>使用不可变结构
此外,null 对于测量没有任何意义。平等和比较也应该遵循衡量规则来实施。
不,我没有看到在您的情况下使用引用类型而不是值类型的相关理由。
I am not sure that performance penalty of boxing/unboxing your value in the UI code should be your main concern here. This perf hit will be minor compared to the layout process for example.
In fact you could formulate your question an other way: Do you want your type to be mutable or immutable? I think immutability would be logical with your specs. It's a value, you said it yourself, by naming it UnitValue. As a developper, I would be rather surprised that an UnitValue is not a value ;) => Use an immutable struct
Furthermore, null does not have any sense for a measurement. Equality and comparaison too should to be implemented following measurement rules.
No, I don't see pertinent reason to use a ref type rather than a value type in your case.
在我看来,您的设计需要元组的值类型语义。 <7.0,毫米>应始终等于<7.0, mm>从程序员的角度来看。 <7.0,毫米>正是其各部分的总和并且没有自己的恒等式。其他一切我都会觉得很混乱。这种 if 也意味着不变性。
现在,是否使用结构或类实现此功能取决于性能以及是否必须支持每个元组的空值。如果您选择结构体,并且只需要在少数情况下支持 null,则可以不用 Nullable。
另外,您不能为您的元组提供一个用于显示目的的引用类型包装器吗?我不太熟悉 WPF,但我想这将消除所有的装箱操作。
In my opinion, your design calls for value type semantics for your tuple. <7.0, mm> should always be equal to <7.0, mm> from a programmers point of view. <7.0, mm> is exactly the sum of its parts and has no own identiy. Everything else I would find very confusing. This kind if implies immutability as well.
Now, if you implement this with structs or classes depends on performance and if you have to support null values for every tuple. If you go for structs you can get away with Nullable if you only need to support null in a few cases.
Also, can't you provide a reference type wrapper for your tuples, which is used for display purposes? I am not to familiar with WPF, but I would imagine that this would eliminate all of the boxing operations.
听起来它具有值语义。该框架提供了一种创建具有值语义的类型的机制,即
struct
。用那个。几乎您在问题的下一段中所说的所有内容,赞成和反对值类型都是基于它如何与运行时的实现细节交互的优化问题。由于这方面有利有弊,因此没有明显的效率赢家。由于如果不实际尝试就无法找到明显的效率获胜者,因此任何在这方面进行优化的尝试显然都为时过早。尽管我对那句话感到厌倦,即当有人试图让某些东西变得更快或更小时,过早的优化就被大肆传播,但它确实适用于此。
但有一件事与优化无关:
根本不正确。 默认是 == 和 != 处理引用相等,但这也是因为它是唯一有意义的默认值,无需更多了解类的语义。当 == 和 != 适合类语义时,应该重载,当引用相等是人们唯一关心的事情时,应该使用 ReferenceEquals。
仅当 == 重载出现新手错误时。正常的方法是:
当然,Equals 重载还应该检查参数是否为 null,如果是,则返回 false,以便人们直接调用它。当一个或两个值都为 null 时,通过默认 == 行为调用此方法甚至不会对性能产生重大影响,那么有什么可担心的呢?
并不真地。就相等性而言,默认语义非常不同,但由于您将某些东西描述为具有值语义,因此倾向于将其作为值类型,而不是作为类类型。除此之外,可用的语义非常相似。这些机制在装箱、参考共享等方面可能有所不同,但这又回到了优化。
我宁愿问,当对类来说这是明智的事情时,重载 == 和 != 是否合理?
至于我作为一名程序员对“UnitValue”的假设,我可能会假设它是一个结构,因为听起来应该是这样。但实际上,我什至不会假设,因为我大多不会关心,直到我用它做一些重要的事情,考虑到它听起来也应该是不可变的,是一个简化集(可变和可变之间的语义差异)引用类型和可变结构在实践中更重要,但这是一个显而易见的不可变结构)。
Sounds like it has value semantics. The framework provides a mechanism for creating types with value semantics, namely
struct
. Use that.Almost everything you say in the next paragraph in your question, both pro and con value-types is a matter of optimising based on how it will interact with for the implementation details of the runtime. Since there are both pros and cons in this regard, there's no clear efficiency winner. Since you can't find the clear efficiency winner without actually trying it, any attempt to optimise in this regard will clearly be premature. As much as I'm sick to death of that quote about premature optimisation being bandied about the moment somebody tries to make something faster or smaller, it does apply here.
One thing though that isn't about optimisation:
Not true at all. The default is that == and != deal with reference equality, but that's as much because the it's the only meaningful default without more knowledge of the semantics of the class. == and != should be overloaded when it fits a classes semantics to do so, ReferenceEquals should be used when reference equality is the only thing one cares about.
Only if the == overload has a newbie mistake. The normal approach would be:
And of course, the Equals overload should also check of the parameter being null and return false if it is, for people calling it directly. There isn't even a significant performance impact on calling this over the default == behaviour when one or both values are null, so what's the concern?
Not really. The default semantics as far as equality goes are pretty different, but since you are describing something as intending to have value semantics, that leans toward having it as a value type, rather than as a class type. Beyond that, the semantics available are much the same. The mechanisms can differ as far as boxing, reference sharing and so-on go, but that's back to optimisation again.
I would rather ask, can not overloading == and != be justified when that's the sensible thing to do for the class?
As for what I as a programmer would assume about "UnitValue", I'd probably assume it was a struct, since it sounds like it should be. But actually, I wouldn't even assume that, as I mostly won't care until I do something with it where it's important, which given that it also sounds like it should be immutable, is a reduced set (the semantic differences between mutable reference types and mutable structs are greater in practice, but this one is a no-brainer immutable).
也许您可以从 Eric Lippert 最近发表的这篇博文。使用结构时要记住的最重要的事情是使它们不可变。这是一个有趣的Jon Skeet 的博客文章,其中可变结构可能会导致非常难以调试的问题。
Maybe you can get some inspiration from this recent blog post by Eric Lippert. Most important thing to remember when using structs is to make them immutable. Here's an interesting blog post by Jon Skeet where a mutable struct can lead to very hard to debug problems.