从技术上讲,您可以在 C# 中执行类似特性的操作。特征是临时多态性。即,您可以临时或事后赠送具有额外特征的类型。当您获得一个类型(例如 BCL 中的 IEnumerable<T>)并且您希望它具有其他属性(例如作为 monad)(但您不拥有该类型,因此您无法更改其接口)时,这很有用列表)。
因此,对于您的迭代器示例,您可以这样做:
public interface Iterator<MA, A>
{
bool HasNext(MA iter);
A Next(MA iter);
}
MA 是容器类型,A 是它包含的内容。
接下来,我们将为IEnumerator<A> 实现它。
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 / HasNext 和Next / 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<MA, A> - 这意味着它不能是null,必须实现Iterator<MA, A>。
然后我们可以使用任何我们喜欢的Iterator 来调用它:
var items = (new[] { 1, 2, 3, 4, 5 }).AsEnumerable().GetEnumerator();
var newitems = IterAnything<IteratorEnum<int>, IEnumerator<int>, int>(items);
这种方法的一个更简单的例子是Num<A> 和Eq<A>。 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);
}
然后我们可以同时实现int 和long:
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# 没有更高的种类,这会导致在尝试实现 Monad 或 Functor 之类的东西时出现问题,但是 types like Monoid are easy