【问题标题】:Read-only ("const"-like) function parameters of C#C# 的只读(类似“const”)函数参数
【发布时间】:2023-03-09 15:38:01
【问题描述】:

来自 C++ 背景,我习惯于将 const 关键字粘贴到函数定义中,以使对象以只读值传递。但是,我发现这在 C# 中是不可能的(如果我错了,请纠正我)。经过一番谷歌搜索,我得出的结论是,制作只读对象的唯一方法是编写一个只有“获取”属性的接口并将其传递进去。优雅,我必须说。

public interface IFoo
{
  IMyValInterface MyVal{ get; }
}

public class Foo : IFoo
{
  private ConcreteMyVal _myVal;

  public IMyValInterface MyVal
  {
    get { return _myVal; }
  }
}

我会把它传递给:

public void SomeFunction(IFoo fooVar)
{
  // Cannot modify fooVar, Excellent!!
}

这很好。但是,在我的其余代码中,我想正常修改我的对象。向界面添加“设置”属性会打破我的只读限制。我可以向Foo(而不是IFoo)添加一个“设置”属性,但签名需要一个接口而不是一个具体的对象。我必须做一些选角。

// Add this to class Foo. Might assign null if cast fails??
set { _myVal = value as ConcreteMyVal; }

// Somewhere else in the code...
IFoo myFoo = new Foo;
(myFoo as Foo).MyFoo = new ConcreteMyVal();

有没有更优雅的方法来复制const 或在不添加其他属性或函数的情况下制作只读函数参数?

【问题讨论】:

  • 只是评论你的第一句话:这不是“const”所做的。所指对象仍然是可变的。例如,考虑一个方法,它需要两个对类型 C 的引用,一个标记为 const,一个不标记。为两个引用传递对同一变量的引用。如果您对非常量引用进行变异,那么常量引用将观察到变异。 const 确实 not 提供了您可以依赖的只读保证。它也不会阻止可变对象的突变,因为它可以被丢弃。这与 C# 中的只读字段非常不同。
  • 解决您的实际问题:这是一个令人困惑的问题。您似乎在说您想要(1)保证只读性和(2)改变对象的能力。那些是对立的。你不能同时拥有它们。您能否解释为什么您希望 SomeFunction 不能诱导突变?这是出于安全目的吗?是为了正确性吗?谁在提供 SomeFunction 的实现?如果是你,那你为什么需要它是 const 呢?如果您不想编写突变,那就不要。如果是其他人,那么为什么要关心他们是否会引起突变?
  • @Eric:限制 SomeFunction 的功能是有意义的,即使我是编写它的人。它增加了编译时检查以验证意外错误。 (我敢肯定你以前犯过错误——你能不能想象自己犯了这种错误,你在不应该的时候不小心改变了一个变量?)我认为提问者不想保证读-only-ness,他只想要一个半保证,即此方法通过参数访问它时不会改变此对象
  • @Eric 我认为这里的想法是编译时错误发现,而不是安全性。我知道 C# 实现了所有“传统”方法来做到这一点,但似乎不太接受最近的方法——比如 Spec# 中正在研究的方法。
  • @Eric:“仅仅让接口提供读取功能可能还不够。” — 为了安全,当然不是!但是对于我没有犯过某种错误的保证,这是一个很好的开始。为什么存在“私人”访问修饰符?显然它不足以保证安全,因为反射可以颠覆它。这是否意味着它不应该存在,我们都应该使用“内部”来代替? “私有”的安全特性与“内部”有何不同?

标签: c# constants


【解决方案1】:

我认为您可能正在寻找一种涉及两个接口的解决方案,其中一个接口继承自另一个接口:

public interface IReadableFoo
{
    IMyValInterface MyVal { get; }
}

public interface IWritableFoo : IReadableFoo
{
    IMyValInterface MyVal { set; }
}

public class Foo : IWritableFoo 
{
    private ConcreteMyVal _myVal;

    public IMyValInterface MyVal
    {
        get { return _myVal; }
        set { _myVal = value as ConcreteMyVal; }
    }
}

然后您可以声明其参数类型“告诉”它是否计划更改变量的方法:

public void SomeFunction(IReadableFoo fooVar)
{
    // Cannot modify fooVar, excellent!
}

public void SomeOtherFunction(IWritableFoo fooVar)
{
    // Can modify fooVar, take care!
}

这模仿了类似于 C++ 中的 constness 的编译时检查。正如 Eric Lippert 正确指出的那样,这与不变性相同。但作为一名 C++ 程序员,我想你知道这一点。

顺便说一句,如果将类中的属性类型声明为ConcreteMyVal,并单独实现接口属性,则可以实现稍微更好的编译时检查:

public class Foo : IWritableFoo 
{
    private ConcreteMyVal _myVal;

    public ConcreteMyVal MyVal
    {
        get { return _myVal; }
        set { _myVal = value; }
    }

    public IMyValInterface IReadableFoo.MyVal { get { return MyVal; } }
    public IMyValInterface IWritableFoo.MyVal
    {
        // (or use “(ConcreteMyVal)value” if you want it to throw
        set { MyVal = value as ConcreteMyVal; }
    }
}

这样,setter 只能在通过接口访问时抛出,而在通过类访问时不能。

【讨论】:

  • 是什么阻止了客户将 IReadable 转换为 IWriteable 并写到他们心中的内容?
  • @Eric:是什么阻止了Console.WriteLine 格式化我的硬盘?除了它试图履行合同之外,什么都没有。
  • 我的观点是,这实际上并不是将类型系统用作强制,而是作为礼貌建议。如果这是目标,那很好,你就完成了。但是我和其他人经常遇到礼貌的建议还不够的情况;如果该值实际上需要提供 no 机制,从而可以通过可验证的类型系统使用来发生突变,那么这是不够的。这与您不能将数组强制转换为 IEnumerable 并返回它的原因相同;有人仍然可以改变数组。如果这是 OP 所处的情况,那么不可变的包装对象会更好。
  • @MoneyOrientedProgrammer:C# 中一个好的设计原则是,产生值的方法应该没有副作用,否则如果你不这样做,就无法计算该值不想要效果。您认为 const 可以防止方法产生副作用的观点是错误的,但很多人都相信这一点,这就是为什么 const 在我看来是一个设计不良的危险特性。
  • @MoneyOrientedProgrammer:我们遇到的问题是可观察效果实现细节效果之间存在很大差异。例如,考虑一个标记为 const 的昂贵递归算法。我们不能作为一种性能改进在不删除 const 的情况下在类中记忆方法,因为记忆化根据定义是一种副作用:它会改变所记忆的内容。我们最终不得不跳出疯狂的圈子来保持 const 的正确性。
【解决方案2】:

首先,您是对的:您不能将 const 或类似关键字应用于 C# 中的参数。

但是,您可以使用接口来执行这些操作。接口在某种意义上是特殊的,制作一个只涵盖功能集特定部分的接口是非常有意义的。例如。 image 一个栈类,它同时实现了 IPopable 和 IPushable。如果您通过 IPopable 接口访问实例,则只能从堆栈中删除条目。如果您通过 IPushable 接口访问实例,则只能将条目添加到堆栈中。您可以通过这种方式使用接口来获得与您所要求的类似的东西。

【讨论】:

    【解决方案3】:

    最接近的等效项是in 关键字。使用in 使参数和输入参数 并防止其在方法内部被更改。来自官方 C# 文档:

    • in - 指定此参数通过引用传递,但仅由被调用的方法读取
    • ref - 指定这个参数是通过引用传递的,可以被调用的方法读写。
    • out - 指定此参数通过引用传递,必须由被调用方法写入。

    【讨论】:

      【解决方案4】:

      首先考虑 Timwi 的回答。但作为第二种选择,您可以这样做,使其更像 C CONST 关键字。

      引用类型(对象)参数默认为 IN 参数。但是因为它们是引用,所以它们的方法副作用和属性访问是对方法之外的对象进行的。该对象不必被传递出去。还是被方法修改了。

      但是,值类型(结构)参数默认也是 IN,并且不能对传入的元素产生副作用或属性修改。相反,它在进入方法之前被 COPIED ON WRITE。当方法超出范围(方法结束)时,对该方法内部的任何更改都会失效。

      不要为了满足这种需求而将你的类更改为结构。这是个坏主意。但是如果它们无论如何都应该是结构,现在你就会知道了。

      顺便说一句,有一半的编程社区没有正确理解这个概念,但认为他们理解(事实上,我在几本书中发现了 C# 中参数方向问题的不准确之处)。如果您想评论我的陈述的准确性,请仔细检查以确保您知道自己在说什么。

      【讨论】:

      • 应该指出的是,使用结构仅具有丢弃对对象的更改的效果,编译器很乐意允许这样做。这意味着方法的作者可以对对象进行更改,然后可能会因为它们不持久而感到困惑。 C++ 中的 const 以及 C# 中基于接口的解决方案的目的是发送一个信号,表明方法不应该进行更改并且编译器应该考虑尝试出错。
      • 不知道你为什么要如此痛苦地强调所有参数默认都是“in”,因为无论如何你都不能使用“out”参数传递值来修改......你的区别在类和结构之间以及它们在按值传递下的行为方式之间-据我所知,输入/输出与它无关。你强调有什么理由吗?
      • @Dan:我不能代表 Phil,但我认为指出“在该方法中对它的任何更改都死掉”的声明仅适用于未标记参数的情况ref.
      • @Timwi:同意,但这与“入”或“出”无关。您不能通过out 参数传递值!按值与按引用与 out 参数不同。
      • @Dan:那不是回答者的意思。我相信他的意思是“在”(默认)而不是outref(必须明确指定)。您不能通过 out 参数传递值,但通过 ref 参数当然可以。
      猜你喜欢
      • 1970-01-01
      • 2019-01-09
      • 1970-01-01
      • 1970-01-01
      • 2013-04-06
      • 2012-02-25
      • 2012-06-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多