【问题标题】:C# interface with default method vs traits具有默认方法与特征的 C# 接口
【发布时间】:2020-04-20 04:50:49
【问题描述】:

在 Scala 文档https://docs.scala-lang.org/tour/traits.html 中,它给出了 trait 的示例。

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}

class IntIterator(to: Int) extends Iterator[Int] {
  private var current = 0
  override def hasNext: Boolean = current < to
  override def next(): Int = {
    if (hasNext) {
      val t = current
      current += 1
      t
    } else 0
  }
}


val iterator = new IntIterator(10)
iterator.next()  // returns 0
iterator.next()  // returns 1

我们知道 C# 还不支持特征。但是,上面的示例可以很容易地转换为 C# 代码:

interface Iterator<A> 
{ 
    bool HasNext(); 
    A Next(); 
}

public class IntIterator : Iterator<int> 
{
    int _to;
    int _current = 0;
    public IntIterator(int to) => _to = to;
    public bool HasNext() => _current < _to;
    public int Next() => HasNext() ? _current++ : 0;
}

var itor = new IntIterator(10);
itor.Next()

C#接口现在可以有默认方法了。与特征相比,C# 缺少什么?

或者应该有一个更好的 Scala 示例来展示 trait 的力量?

【问题讨论】:

  • @Renat 不,他们不是。

标签: c# scala functional-programming


【解决方案1】:

从技术上讲,您可以在 C# 中执行类似特性的操作。特征是临时多态性。即,您可以临时或事后赠送具有额外特征的类型。当您获得一个类型(例如 BCL 中的 IEnumerable&lt;T&gt;)并且您希望它具有其他属性(例如作为 monad)(但您不拥有该类型,因此您无法更改其接口)时,这很有用列表)。

因此,对于您的迭代器示例,您可以这样做:

public interface Iterator<MA, A>
{
    bool HasNext(MA iter);
    A Next(MA iter);
}

MA 是容器类型,A 是它包含的内容。

接下来,我们将为IEnumerator&lt;A&gt; 实现它。

public struct IteratorEnum<A> : Iterator<IEnumerator<A>, A>
{
    public bool HasNext(IEnumerator<A> iter) =>
        iter.MoveNext();

    public A Next(IEnumerator<A> iter) =>
        iter.Current;
}

忽略MoveNext / HasNextNext / Current 不是同一个意思,这只是为了方便。

注意类型是struct。这是因为结构永远不能是null。因此,default(MY_STRUCT) 将始终具有非空值。

现在,我们将创建一个使用 Iterator 且对底层类型一无所知的通用实现:

public static IEnumerable<A> IterAnything<IterA, MA, A>(MA iter) 
    where IterA : struct, Iterator<MA, A>
{
    while(default(IterA).HasNext(iter))
    {
        yield return default(IterA).Next(iter);
    }
}

注意default(IterA) 的使用,这是因为我们将IterA 限制为struct, Iterator&lt;MA, A&gt; - 这意味着它不能是null,必须实现Iterator&lt;MA, A&gt;

然后我们可以使用任何我们喜欢的Iterator 来调用它:

var items = (new[] { 1, 2, 3, 4, 5 }).AsEnumerable().GetEnumerator();

var newitems = IterAnything<IteratorEnum<int>, IEnumerator<int>, int>(items);

这种方法的一个更简单的例子是Num&lt;A&gt;Eq&lt;A&gt;。 C# 中没有基本的INumeric 类型,但我们想编写一次数值处理函数。类型都隐藏在 BCL 中,所以我们不能去改变它们的接口:

public interface Num<A>
{
    A Add(A lhs, A rhs);
    A Subtract(A lhs, A rhs);
    A Multiply(A lhs, A rhs);
    A Divide(A lhs, A rhs);
    A FromInt(int value);
    A One { get; }
    A Zero { get; }
}

public interface Eq<A>
{
    bool IsEqualTo(A lhs, A rhs);

    // using C#8 default interface methods
    bool IsNoEqualTo(A lhs, A rhs) => !IsEqualTo(lhs, rhs); 
}

然后我们可以同时实现intlong

public struct NumInt : Num<int>, Eq<int>
{
    public int Add(int lhs, int rhs) => lhs + rhs;
    public int Subtract(int lhs, int rhs) => lhs - rhs;
    public int Multiply(int lhs, int rhs) => lhs * rhs;
    public int Divide(int lhs, int rhs) => lhs / rhs;
    public int FromInt(int value) => value;
    public bool IsEqualTo(int lhs, int rhs) => lhs == rhs;
    public int One => 1;
    public int Zero => 0;
}

public struct NumLong : Num<long>, Eq<long>
{
    public long Add(long lhs, long rhs) => lhs + rhs;
    public long Subtract(long lhs, long rhs) => lhs - rhs;
    public long Multiply(long lhs, long rhs) => lhs * rhs;
    public long Divide(long lhs, long rhs) => lhs / rhs;
    public long FromInt(int value) => (long)value;
    public bool IsEqualTo(long lhs, long rhs) => lhs == rhs;
    public long One => 1;
    public long Zero => 0;
}

然后我们可以创建一些处理数字的方法:

public static bool IsEqualTo0<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
    default(NumA).IsEqualTo(n, default(NumA).Zero);

public static bool IsEqualTo1<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
    default(NumA).IsEqualTo(n, default(NumA).One);

public static A Subtract1<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
    default(NumA).Subtract(n, default(NumA).One);

public static A Subtract2<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
    Subtract1<NumA, A>(Subtract1<NumA, A>(n));

public static A Fibonacci<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
    IsEqualTo0<NumA, A>(n) || IsEqualTo1<NumA, A>(n)
        ? n
        : Fibonacci<NumA, A>(default(NumA).Add(
                                Subtract1<NumA, A>(n), 
                                Subtract2<NumA, A>(n)));

最后,调用:

Fibonacci<NumInt, int>(100);
Fibonacci<NumLong, long>(100L);

因此,这显示了向临时类型添加特征。这并不漂亮,C# 团队正在寻求通过 Shapes/Concepts/.. 提案来解决这个问题。但是,如果你需要它是可能的。我不时使用它,并且拥有a ton of example traits and implementations in my language-ext library(称为类型类和类实例)。

如果 .NET 团队创建了一个 INumeric 接口并让所有数字类型都派生自它,那么使用它会导致装箱。上面演示的方法根本不会导致任何拳击。此外,所有default(NumA) 调用都在发布版本中得到优化,因此与直接调用函数一样快。

限制是 C# 没有更高的种类,这会导致在尝试实现 MonadFunctor 之类的东西时出现问题,但是 types like Monoid are easy

【讨论】:

    【解决方案2】:

    特别是与 Scala 特征相比(不同语言的特征可能大不相同),缺少 C#:

    1. 初始化代码:可以有

      trait A {
        println("A's constructor")
      }
      

      并且此代码将在继承自A 的任何类的构造函数中执行(以正确的顺序)。或者更简单

      trait A {
        val x = 10
      }
      
    2. Trait linearization(至少在具体细节上)

    3. base(在 C# 中)/super(在 Scala 中)分辨率不同,这意味着 Stackable Trait 模式不适用于 C# 接口。

    4. (来自 Scala 3)Constructor parameters

    5. (在 Scala 3 中已删除)Early definitions

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-08-16
      • 2018-01-07
      • 2015-07-31
      • 1970-01-01
      • 2015-01-11
      • 2019-04-02
      • 1970-01-01
      相关资源
      最近更新 更多