【问题标题】:Force function input parameters to be immutable?强制函数输入参数不可变?
【发布时间】:2020-07-14 09:33:11
【问题描述】:

我刚刚花了 2 天的时间试图找出一个错误,结果发现我不小心改变了作为函数输入提供的值。

    IEnumerable<DataLog>
    FilterIIR(
        IEnumerable<DataLog> buffer
    ) {
        double notFilter = 1.0 - FilterStrength;

        var filteredVal = buffer.FirstOrDefault()?.oilTemp ?? 0.0;
        foreach (var item in buffer)
        {
            filteredVal = (item.oilTemp * notFilter) + (filteredVal * FilterStrength);

            /* Mistake here!
            item.oilTemp = filteredValue;
            yield return item;
            */

            // Correct version!
            yield return new DataLog()
            {
                oilTemp = (float)filteredVal,
                ambTemp = item.ambTemp,
                oilCond = item.oilCond,
                logTime = item.logTime
            };
        }
    }

我的首选编程语言通常是 C# 或 C++,具体取决于我认为更适合需求的语言(这是更适合 C# 的大型程序的一部分)...

现在在 C++ 中,我可以通过接受常量迭代器来防止此类错误,这会阻止您在检索它们时修改值(尽管我可能需要为返回值构建一个新容器) .我做了一些搜索,在 C# 中找不到任何简单的方法,有没有人知道不同的方法?

我在想我可以创建一个 IReadOnlyEnumerable&lt;T&gt; 类,它将 IEnumerable 作为构造函数,但后来我意识到,除非它在您检索它们时复制值,否则它实际上不会有任何效果,因为基础价值仍然可以修改。

我有什么办法可以防止将来出现此类错误?一些包装类,或者即使它是我要保护的每个函数顶部的一个小代码 sn-p,真的什么都可以。

目前我能想到的唯一可行的方法是为我需要的每个类定义一个ReadOnly 版本,然后有一个非只读版本来继承和重载属性并添加函数提供同一类的可变版本。

【问题讨论】:

  • 您是否考虑过让DataLog 成为一个不可变的类。
  • 感谢@Salah 的格式检查,虽然realised 不是一个错误XD
  • @juharr 我在上一段中提到过,除非这不是你的意思?
  • 我是说拥抱不可变并且不要为类的可变版本而烦恼。
  • @TheBeardedQuack 不客气。你是对的,这不是一个错误,但拼写检查器建议改用“realized”,这就是我改变它的原因:)

标签: c# function parameters immutability


【解决方案1】:

这里的问题与IEnumerable 无关。 IEnumerables 实际上是不可变的。您不能在其中添加或删除内容。可变的是您的 DataLog 类。

因为DataLog 是引用类型,item 持有对原始对象的引用,而不是对象的副本。这一点,加上DataLog 是可变的这一事实,允许您改变传入的参数。

所以在高层次上,您可以:

  • 复制DataLog,或者;
  • 使DataLog 不可变

或两者兼有……

您现在正在做的是“复制DataLog”。另一种方法是将DataLogclass 更改为struct。这样,您将始终在将其传递给方法时创建它的副本(除非您使用ref 标记参数)。因此,在使用此方法时要小心,因为它可能会默默地破坏假定传递引用语义的现有方法。

您还可以使DataLog 不可变。这意味着删除所有的二传手。或者,您可以添加名为 WithXXX 的方法,该方法返回对象的副本,其中只有一个属性不同。如果您选择这样做,您的 FilterIIR 将如下所示:

yield return item.WithOilTemp(filteredVal);

目前我能想到的唯一可行的方法是为我需要的每个类定义一个只读版本,然后有一个非只读版本继承和重载属性并添加函数来提供同一类的可变版本。

您实际上不需要这样做。请注意List&lt;T&gt; implements IReadOnlyList&lt;T&gt; 是如何变化的,尽管List&lt;T&gt; 显然是可变的。您可以编写一个名为IReadOnlyDataLog接口。这个接口只有DataLoggetters。然后,让FilterIIR 接受IEnumerable&lt;IReadOnlyDataLog&gt;DataLog 实现IReadOnlyDataLog。这样,您就不会意外地改变 FilterIIR 中的 DataLog 对象。

【讨论】:

  • 嗨@Sweeper,我没想过使用接口方法,但这实际上是我创建只读基类然后继承的意思。接口方法要简洁得多,因为那时我只有 1 个包含实现的位置。我知道IEnumerable 已经是不可变的,也许我可以更好地解释自己。当我在 C++ 中有一个 const 容器时,它还会通过让所有访问器返回常量引用来强制它包含的项目的不变性。
  • 虽然WithX() 方法很有趣,但我认为这不会产生特别漂亮的代码库。 +1 为界面建议,虽然我不知何故完全错过了。
  • @TheBeardedQuack 好吧,C# 不是 C++,所以你不能直接翻译所有内容。将英文文本逐字翻译成德语会产生听起来很奇怪的德语,不是吗? :) 您只需要使用 语言提供的功能。 IMO,IReadOnlyDataLog 方法也是最好的总体
  • 是的,我明白这一点,而且 C# 肯定有某些我希望在 C++ 中可用的做事方式。我试图用 C++ 来解释我的观点,然后询问是否有任何类似的机制。如前所述,我确实认为界面方法是一个不错的主意。我确实经常使用接口来帮助定义我的自定义类型,但我通常不需要定义我自己的类型,所以我可能不习惯在这个庄园里思考我应该的样子。将来我可能会更多地使用这种方法......如果没有更好的建议(针对这种情况)出现,我很乐意将其标记为答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-17
  • 2020-05-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多