【问题标题】:Should I use struct or class?我应该使用结构还是类?
【发布时间】:2023-03-14 00:05:01
【问题描述】:

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

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

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

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

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

所以我的问题是:

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

任何情况下,类中的 == / != 重载是否合理?

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

根据我的使用说明,你会选择什么?

【问题讨论】:

    标签: c# class-design boxing


    【解决方案1】:

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

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

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

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

    例如,如果我声明了一个类型Foo,它定义了一个总是返回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 是相当违反直觉的),但从根本上说,它只是意味着你不应该能够改变实例中的状态值,从而改变比较操作的结果。如果在您的场景中能够改变 一些 实例状态信息而不影响比较结果是有意义的,那么没有理由不应该这样做。这只是极少数情况。

    【讨论】:

      【解决方案2】:

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

      不,约定略有不同:

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

      不,同样的讨论。

      关键不在于一个类型是否是一个reference类型。 – 类型是否表现作为一个值。这对于String 是正确的,对于您希望重载operator ==!= 的任何类都应该如此。

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

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

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

      【讨论】:

        【解决方案3】:

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

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

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

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

        【讨论】:

        • 这些类型在UI以外的地方使用,但需要存储在一个“对象”中以供UI呈现。我担心的是将来有人会在性能重的表达式中使用该类型。
        • @Holstebroe,但是如果有人用它做一些性能很重的事情,为什么要把它作为一个对象访问并处理所有的装箱和拆箱?如果某些东西被证明是性能重的,那么他们会查看它,将其更改为直接处理结构的类型,问题就消失了,并且可能(取决于性能重的东西是什么)超过参考类型。
        【解决方案4】:

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

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

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

        【讨论】:

          【解决方案5】:

          用于包含值和测量单位元组的数据结构(例如 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”的假设,我可能会假设它是一个结构,因为它听起来应该是。但实际上,我什至不会假设,因为我大多不在乎,直到我在重要的地方对它做一些事情,考虑到它听起来也应该是不可变的,所以它是一个缩减集(可变之间的语义差异引用类型和可变结构在实践中更大,但这是不可变的)。

          【讨论】:

            【解决方案6】:

            也许您可以从 Eric Lippert 的 this recent blog post 获得一些灵感。使用结构时要记住的最重要的事情是make them immutable。这是 Jon Skeet 的一个有趣的 blog post,其中可变结构可能导致非常难以调试的问题。

            【讨论】:

            • 我只是在想这个......再次。我们将使它成为有史以来最受欢迎的帖子:)
            • 我实际上认为这有可能成为那些被视为规则的准则之一,因为有些地方可变结构是有意义的。同样,不可变引用类型也相对被低估了。尽管如此,对于大多数结构来说都是如此,在这个问题中,不变性比值与引用类型更简单。
            猜你喜欢
            • 2016-10-21
            • 1970-01-01
            • 1970-01-01
            • 2011-09-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-10-01
            • 1970-01-01
            相关资源
            最近更新 更多