为啥 C# 中的诚实函数示例仍然不诚实?

Posted

技术标签:

【中文标题】为啥 C# 中的诚实函数示例仍然不诚实?【英文标题】:Why honest function example in C# still not being honest?为什么 C# 中的诚实函数示例仍然不诚实? 【发布时间】:2020-07-15 19:44:30 【问题描述】:

来自此参考:http://functionalprogrammingcsharp.com/honest-functions

在 C# 中定义方法/函数时,我学会了更加诚实。 它表示更喜欢纯函数,以便函数始终给出签名中给出的确切返回类型。

但是当我尝试应用它时:

int Divide(int x, int y)

    return x / y;

来自网站:

签名声明该函数接受两个整数并返回另一个整数。但并非在所有情况下都如此。如果我们调用像 Divide(1, 0) 这样的函数会发生什么?函数实现不遵守其签名,抛出 DivideByZero 异常。这意味着这个函数也是“不诚实的”。我们如何才能使这个函数成为一个诚实的函数?我们可以更改 y 参数的类型(NonZeroInteger 是自定义类型,可以包含除零以外的任何整数):

int Divide(int x, NonZeroInteger y)

    return x / y.Value;

我不确定 NonZeroInteger 的实现是什么,他们似乎没有在网站中提供 NonZeroInteger 的任何实现,是否应该在该类中检查 0?和 我很确定如果我调用 Divide(1, null) 它仍然会显示错误,从而使函数不诚实。

为什么C#中的诚实函数示例仍然不诚实?

【问题讨论】:

我不确定是否存在语言障碍,但据我所知,“诚实”不是一个技术术语,我无法从您对在这种情况下的词。您能尝试用技术和客观的措辞来描述问题吗? 我已经阅读了链接,我认为作者有点懒惰,只是在没有任何细节的情况下将NonZeroInteger 类型扔出去。 我真的不明白如此严格地坚持这一点有什么意义。显然,在一定程度上“诚实”是有好处的,但如果你正在追踪每一种可能的“不诚实”可能性(这在 IMO 中是不可能的),那么你就是在浪费 很多可以花在更好的事情上的时间。更不用说你只是将可能性推向其他地方(即NonZeroInteger 的实现现在将如何处理 0?) 大概需要某种“CreateNonZeroIntegerOrThrowExceptionIfZero”工厂方法才能 100% 诚实,但是就像@BrootsWaymb 所说的那样......有什么意义?您只是将责任推到了似乎超过收益的程度。 @KevinTanudjaja - 任何相当大的应用程序都不会没有错误。关于“Hello World”类型应用程序之外的复杂性,这是一个幻想。只是为了“诚实”而增加的复杂性似乎有可能引入更多错误,或者至少是一些令人头疼的问题和额外的工作。这种做法在合理的情况下很好,但不要像法律/圣经一样对待它 【参考方案1】:

我只想提一下,NonZeroInteger 绝对可以使用Peano numbers 的变体诚实地实现:

class NonZeroInteger

    /// <summary>
    /// Creates a non-zero integer with the given value.
    /// (This is private, so you don't have to worry about
    /// anyone passing in 0.)
    /// </summary>
    private NonZeroInteger(int value)
    
        _value = value;
    

    /// <summary>
    /// Value of this instance as plain integer.
    /// </summary>
    public int Value
    
        get  return _value; 
    
    private readonly int _value;

    public static NonZeroInteger PositiveOne = new NonZeroInteger(1);
    public static NonZeroInteger NegativeOne = new NonZeroInteger(-1);

    /// <summary>
    /// Answers a new non-zero integer with a magnitude that is
    /// one greater than this instance.
    /// </summary>
    public NonZeroInteger Increment()
    
        var newValue = _value > 0
            ? _value + 1    // positive number gets more positive
            : _value - 1;   // negative number gets more negative
        return new NonZeroInteger(newValue);   // can never be 0
    

唯一棘手的部分是我定义了Increment,以便它可以同时处理正整数和负整数。您可以创建除零之外的任何整数值,并且不会抛出任何异常,所以这个类是完全诚实的。 (我暂时忽略溢出,但我不认为这会是个问题。)

是的,它需要你重复加一来构建大整数,这是非常低效的,但是对于像这样的玩具类来说这没问题。可能还有其他更有效的诚实实现(例如,使用 uint 作为 +1 或 -1 的偏移量),但我将把它作为练习留给读者。

你可以这样测试:

class Test

    static int Divide(int x, NonZeroInteger y)
    
        return x / y.Value;
    

    static void Main()
    
        var posThree = NonZeroInteger.PositiveOne
            .Increment()
            .Increment();
        Console.WriteLine(Divide(7, posThree));

        var negThree = NonZeroInteger.NegativeOne
            .Increment()
            .Increment();
        Console.WriteLine(Divide(7, negThree));
    

输出是:

2
-2

【讨论】:

【参考方案2】:

读了很多遍..

我发现他在网站上的第二个选项是诚实的,而第一个是错误的。

int? Divide(int x, int y)
    
        if (y == 0)
        return null;
        return x / y;
    

编辑:从另一篇文章中得到想法,基本上模仿了 F# 路径,如下所示:

Option<int> Divide(int x, int y)
    
        if (y == 0)
            return Option<int>.CreateEmpty();
        return Option<int>.Create(x / y);
    

public class Option<T> : IEnumerable<T>

    private readonly T[] _data;

    private Option(T[] data)
    
        _data = data;
    

    public static Option<T> Create(T element)
    
        return new Option<T>(new T[]  element );
    

    public static Option<T> CreateEmpty()
    
        return new Option<T>(new T[0]);
    

    public IEnumerator<T> GetEnumerator()
    
        return ((IEnumerable<T>) _data).GetEnumerator();
    

    IEnumerator IEnumerable.GetEnumerator()
    
        return GetEnumerator();
    

    public void Match(Action<T> onSuccess, Action onError) 
        if(_data.Length == 0) 
            onError();
         else 
            onSuccess(_data[0]);
        
    



参考:https://www.matheus.ro/2017/09/26/design-patterns-and-practices-in-net-option-functional-type-in-csharp/

打电话:

public void Main() 
    Option<int> result = Divide(1,0);
    result.Match(
        x => Console.Log(x),
        () => Console.Log("divided by zero")
    )

【讨论】:

我不确定这是否“诚实”。使用null 作为魔法值有点小技巧。 是的,这是不诚实的:) 本文对空值的处理不一致。在第一个示例中,尽管字符串可以为空,但空值是“坏的”。空字符串仍然是字符串。稍后返回 null 的可空 int 是合法的。就像@Sean 说的那样,这是一个 hack,就像作者自己认为它是一个带有字符串的 hack。我不明白为什么应该区别对待。 如果null 是某些操作的有效值,您的选项类将不起作用。 @Sean 你的意思是在方法周围传递空值?【参考方案3】:

“诚实功能”的概念仍有解释的余地​​,我不想在这里争论,这将是更多的意见而不是实际有用的答案。

要具体回答您的示例,您可以将 NonZeroInteger 声明为 ValueType,使用 struct 而不是 class

值类型是不可为空的(除非您使用? 明确指定可空版本)。在这种情况下没有空问题。顺便说一句,int 是值类型的一个示例(准确地说,它是System.Int32 的别名)。

因为有些有pointed out,它可能会导致其他困难(结构总是有一个默认构造函数将所有字段初始化为默认值,int 的默认值是 0...)

对于一个中等经验的程序员来说,这种例子不需要在文章中明确实现即可理解。

但是,如果您不确定,这绝对是一个很好的编程学习练习,我强烈建议您自己实现它! (顺便说一下,创建单元测试来证明你的函数没有“错误”)

【讨论】:

即使我把它做成一个结构,创建构造函数不允许0值,我仍然需要提供一种方法来处理它是否被0除,它基本上是说如果被除则返回0由 0 @KevinTanudjaja :我试图专注于“非零整数”。在这里,您可以定义一个由私有 int 字段支持的公共属性。在属性的getter 中,如果字段为零,则只需返回另一个随机值(例如 1)。很丑,我不推荐,但符合永不返回零的要求,并且也可以避免任何null【参考方案4】:

老实说,国际海事组织,这是矫枉过正,但如果我要采用“诚实”的方法。我会做这样的事情。而不是创建一个全新的类。这不是关于做什么的建议,以后很容易导致代码出现问题。 IMO 处理此问题的最佳方法是使用该函数并在方法之外捕获异常。这是一个“诚实”的方法,因为它总是返回一个整数,但它可能返回错误值。

    int Divide(int x, int y)
    
        try
        
            return x / y;
        
        catch (DivideByZeroException)
        
            return 0;
        
    

【讨论】:

我不会说这是诚实的。如果你除以零,返回 0 不仅在数学上完全不正确,而且它掩盖了代码中其他地方可能出现的错误(如果这个方法永远不应该传递一个零怎么办?好吧,现在你不能说有什么问题了)。 为什么不先检查一下y 是否为零? 这是一个有效的答案。就它总是返回一个 int 而言,它是“诚实的”。 @PatrickMcvay - 嗯...如果 x 为零怎么办?合法地使除法等于零是微不足道的。 不管怎样。我确实知道将其放入您的代码中是很可怕的。我不是在争论这一点。【参考方案5】:

这个NonZeroInteger只是一个“符号”,只是代表想法,不代表具体实现。

当然,作者可以提供这种结构的实现,但它的名称服务器正好适合一篇文章。

可能的实现可能是:

public class NonZeroInteger

  public int Value  get; set; 
  public NonZeroInteger(int value)
  
    if( value == 0 ) throw new ArgumentException("Argument passed is zero!");
    Value = value;
  

但这只是将不诚实推到其他地方(就文章而言),因为构造函数应该返回一个对象,而不是抛出异常。

IMO,诚实是无法实现的,因为它只是将不诚实转移到其他地方,如本例所示。

【讨论】:

这行不通。所有structs 都带有一个内置的默认构造函数(您无法更改)。默认构造函数会将所有成员设置为其默认值。因此,Value 属性的支持字段将设置为零。我怀疑如果你创建了一个不可变类(如System.String),你可能会接近,但你会失去值类型语义(例如,NonZeroInteger 实例的数组会将所有数组项设置为@987654327 @初始化)。我想几乎没有办法做到这一点 是的,关于默认构造函数 @Flydog57 很好。它看起来更复杂...... 哦,好吧..我也相信文章 NonZeroInteger 是无法实现的。 @KevinTanudjaja 这是可以实现的。例如,您可以仅通过属性公开该值,并确保该属性的 getter 永远不会返回 0 ......但如果这里的数据结构不是试图“隐藏问题”,它肯定对程序员更有帮助",而是抛出异常。而隐瞒真相/问题恰恰与英文honest这个词的意思相反!【参考方案6】:

老实说,定义一个新的数据结构并检查状态。

enum Status  OK, NAN 
class Data

    public int Value  get; set; 
    public Status Status  get; set; 

    public static Data operator /(Data l, Data r)
    
        if (r.Value == 0)
        
            // Value  can be set to any number, here I choose 0. 
            return new Data  Value = 0, Status = Status.NAN ;
        
        return new Data  Value = l.Value / r.Value, Status = Status.OK ;
    

    public override string ToString()
    
        return $"Value: Value, Status: Enum.GetName(Status.GetType(), Status)";
    


class Test

    static Data Divide(Data left, Data right)
    
        return left / right;
    
    static void Main()
    
        Data left = new Data  Value = 1 ;
        Data right = new Data  Value = 0 ;
        Data output = Divide(left, right);

        Console.WriteLine(output);
    

【讨论】:

是的,我认为要么是上面的 TryGetValue 答案,要么创建新的数据结构,要么制作元组!【参考方案7】:

以您发布的示例为例,并阅读链接后,如果您想让函数“诚实”,那么您实际上不需要创建新类型,您可以实现 Try 模式:

bool TryDivide(int x, int y, out int result)

  if(y != 0)
  
    result = x / y;
    return true;
  

  result = 0;
  return false;

这个功能基本上符合“诚实”的原则。名字说它会尝试做除法,结果 'bool' 说它会表明它是成功的。

您可以创建一个struct NonZeroInteger,但您将不得不围绕它编写大量代码以使其表现得像一个常规的数字类型,而且您可能会回到原点。例如,如果将0 传递给NonZeroInteger 构造函数会怎样?它应该失败吗?那是诚实的吗?

另外,struct 类型总是有一个默认构造函数,所以如果你要包装一个 int,避免它被设置为 0 会很尴尬。

【讨论】:

是的,您可以创建一个永远不会返回 0 的 get 属性,但肯定很尴尬。 @Pac0 - 但这只是意味着该物业不“诚实”!

以上是关于为啥 C# 中的诚实函数示例仍然不诚实?的主要内容,如果未能解决你的问题,请参考以下文章

赴美生子诚实签

函数式代码是诚实代码

失误的程序员

半诚实模型

半诚实模型

半诚实模型