【问题标题】:"const correctness" in C#C# 中的“常量正确性”
【发布时间】:2010-09-11 23:09:28
【问题描述】:

常量正确性的重点是能够提供用户无法更改或删除的实例视图。编译器通过指出何时从 const 函数中破坏 const 或尝试使用 const 对象的非 const 函数来支持这一点。因此,在不复制 const 方法的情况下,有没有可以在 C# 中使用的具有相同目的的方法?

我知道不可变性,但这并没有真正延续到容器对象的名称中,只是一个例子。

【问题讨论】:

标签: c# theory const-correctness


【解决方案1】:

C# 没有这样的功能。您可以按值或按引用传递参数。除非您指定 ref 修饰符,否则引用本身是不可变的。但是引用的数据不是一成不变的。所以如果你想避免副作用,你需要小心。

MSDN:

Passing Parameters

【讨论】:

  • 嗯,我想这就是问题所在:避免没有 const 结构的副作用的最佳方法是什么?
  • 不幸的是,只有不可变类型可以帮助它。你可以看看 Spec# - 有一些有趣的编译时检查。
【解决方案2】:

要获得 const-craziness(或函数式编程术语中的纯粹性)的好处,您需要以一种不可变的方式设计您的类,就像 c# 的 String 类一样。

这种方法比仅仅将对象标记为只读要好得多,因为使用不可变类,您可以在多任务环境中轻松传递数据。

【讨论】:

  • 但是不变性并不能真正扩展到复杂的对象,或者是吗?
  • 我认为,如果您的对象如此复杂以至于无法实现不变性,那么您将有一个很好的重构候选对象。
  • 我认为这个是小组中最好的。不可变对象的使用频率太低。
  • 不仅如此,在某些情况下,您确实希望更改对象(对象确实会更改!)但在大多数情况下仍提供只读视图。不可变对象意味着每当您需要进行更改(发生更改)时,您将需要创建一个新对象,其中包含更改之外的所有相同数据。考虑一个有教室的学校,学生......你想在学生的生日和年龄改变时创建一所新学校吗?或者您可以在学生级别更改年龄,在房间级别更改学生,或者在学校级别更改房间?
  • @tenpn:如果做得好,不变性实际上扩展得非常好。真正有用的一件事是将 Builder 类用于大型不可变类型(Java 和 .NET 定义了 StringBuilder 类,这只是一个示例)。
【解决方案3】:
  • const 关键字可用于基本类型和字符串等编译时常量
  • readonly 关键字可用于引用类型等运行时常量

readonly 的问题在于它只允许引用(指针)保持不变。引用(指向)的事物仍然可以修改。这是棘手的部分,但没有办法绕过它。实现常量对象意味着让它们不暴露任何可变的方法或属性,但这很尴尬。

另请参阅Effective C#: 50 Specific Ways to Improve Your C#(第 2 项 - 首选只读而不是 const。)

【讨论】:

    【解决方案4】:

    我也遇到过很多次这个问题,最终使用了接口。

    我认为放弃 C# 是任何形式,甚至是 C++ 演变的想法很重要。它们是两种不同的语言,语法几乎相同。

    我通常通过定义一个类的只读视图来表达 C# 中的“常量正确性”:

    public interface IReadOnlyCustomer
    {
        String Name { get; }
        int Age { get; }
    }
    
    public class Customer : IReadOnlyCustomer
    {
        private string m_name;
        private int m_age;
    
        public string Name
        {
            get { return m_name; }
            set { m_name = value; }
        }
    
        public int Age
        {
            get { return m_age; }
            set { m_age = value; }
        }
    }
    

    【讨论】:

    • 当然可以,但是如果您的某个字段是列表或富类型怎么办。您的解决方案很快就会变得复杂。
    • @Trap:这就是问题的重点。返回内部集合是一种不好的做法,因为外部世界可能会修改您的内部,在 C++ 中,这可以通过使用 const 解决:您提供集合的常量视图(不能添加/删除元素,仅提供其常量视图元素),因此它是安全的(并且是一个常见的习语)。无需设计接口或其他技巧来避免外部代码更改您的内部。
    • @Trap,我同意该机制有助于开发,其中包括检测开发人员的错误,无论是否有经验。我不同意必须编写只读接口在几个方面是一个更好的解决方案。第一点是,每当您在 C++ 中编写一个类时,您都在隐式定义常量接口:声明为const 的成员子集,而无需定义单独的接口和需要运行时调度的额外成本。也就是说,该语言提供了一种实现编译时常量接口的简单方法。
    • 重要的是要注意const-ness 设计的一部分,它像编写外部常量接口一样清楚地声明了意图。此外,如果必须手动处理,提供 const 接口可能是一项复杂的任务,并且可能需要编写几乎与复合类型中存在的类一样多的接口。这将我们带到你的最后一句话:'由于忘记添加 const 而把事情搞砸了'。习惯于到处添加const(开发成本很小)比为每个类编写只读接口更容易。
    • 当 C# 出现时,我真正喜欢它的原因之一是它没有像 Java 那样抛弃 C++ 的所有实用特性。 Java 尝试用接口做所有事情,结果是一场灾难。证据就在布丁里。随着时间的推移,许多最初被谴责为异端的特性被“重新”添加到 Java 中。基于接口的设计有它的位置,但是当采取极端措施时,可能会使程序的体系结构不必要地复杂化。您最终会花费更多时间编写样板代码,而花费更少的时间来完成有用的事情。
    【解决方案5】:

    接口就是答案,实际上比 C++ 中的“const”更强大。 const 是针对“const”被定义为“不设置成员或调用设置成员的东西”的问题的一刀切解决方案。在许多情况下,这是 const-ness 的一个很好的简写,但不是所有情况。例如,考虑一个基于某些成员计算值但也缓存结果的函数。在 C++ 中,这被认为是非常量,尽管从用户的角度来看,它本质上是 const。

    接口使您可以更灵活地定义要从类中提供的特定功能子集。想要常量吗?只需提供一个没有变异方法的接口。想要允许设置一些东西而不是其他东西?提供仅包含这些方法的接口。

    【讨论】:

    • 不是 100% 正确的。允许 C++ const 方法改变标记为 mutable 的成员。
    • 很公平。并且 const-casting 可以让你摆脱 const-ness。两者都暗示即使是 C++ 设计者也意识到一刀切实际上是一刀切。
    • C++ 的优点是大多数情况下你有 const,你可以为其他人实现接口。现在,除了 const 之外,C++ 还具有 mutable 关键字,可应用于属性作为数据缓存或锁定机制(互斥锁等)。 const 不是“不会改变任何内部属性”,而是不会改变从外部感知的对象状态。也就是说,在调用 const 方法之前和之后对该对象的任何使用都会产生相同的结果。
    • 在 C++ 中抛弃 const 来改变 comething 是不可抗拒的行为(鼻恶魔)。 const_cast 用于与在逻辑上为 const 时未标记为 const 的遗留代码进行交互。
    【解决方案6】:

    我只是想提醒您,许多 System.Collections.Generics 容器都有一个 AsReadOnly 方法,该方法将为您返回一个不可变的集合。

    【讨论】:

    • 即使 ReadOnlyCollection 不是,这仍然存在 T 是可变的问题。
    【解决方案7】:

    同意其他人的观点,即使用您在构造函数中初始化的只读字段来创建不可变对象。

        public class Customer
        {
        private readonly string m_name;
        private readonly int m_age;
    
        public Customer(string name, int age)
        {
            m_name = name;
            m_age = age;
        }
    
        public string Name
        {
            get { return m_name; }
        }
    
        public int Age
        {
            get { return m_age; }
        }
      }
    

    或者,您还可以在属性上添加访问范围,即公共获取和保护集?

        public class Customer
        {
        private string m_name;
        private int m_age;
    
        protected Customer() 
        {}
    
        public Customer(string name, int age)
        {
            m_name = name;
            m_age = age;
        }
    
        public string Name
        {
            get { return m_name; }
            protected set { m_name = value; }
        }
    
        public int Age
        {
            get { return m_age; }
            protected set { m_age = value; }
        }
      }
    

    【讨论】:

    • 这些不同的方法并不真正适合整个问题。使用访问控制级别,您只允许从更改中派生类,在许多情况下,这不会适当地模拟现实世界。老师不是从学生记录中得出的,但可能想要更改学生的成绩,尽管学生不能更改成绩但可以阅读它们……仅举一个简单的例子。
    猜你喜欢
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    • 2011-06-30
    • 2012-02-07
    • 1970-01-01
    • 1970-01-01
    • 2020-11-05
    相关资源
    最近更新 更多