从未来看 C#
前言
如今 C# 虽然发展到了 8.0 版本,引入了诸多的函数式特性,但其实在 C# 未来的规划当中,还有很多足以大规模影响现有 C# 代码结构和组成的特性,本文中将会对就重要的特性进行介绍,并用代码示例展示这些特性。
以下特性将会在 C# 9.0、10.0 或者更高版本提供。
Records
Records 是一种全新的简化的 C# class
和 struct
的形式。
现在当我们需要声明一个类型用来保存数据,并且支持数据的解构的话,需要像如下一样写出大量的样板代码:
class Point : IEquatable<Point>
{
public readonly double X;
public readonly double Y;
public Point(double X, double Y)
{
this.X = X;
this.Y = Y;
}
public static bool operator==(Point left, Point right) { ... }
public bool Equals(Point other) { ... }
public override bool Equals(object other) { ... }
public override int GetHashCode() { ... }
public void Deconstruct(out double x, out double y) { ... }
}
十分复杂。引入 Records 之后,上面的样板代码只需简化成一句话:
data class Point(double X, double Y);
并且 Records 支持数据的变换、解构和模式匹配:
var pointA = new Point(3, 5);
var pointB = pointA with { Y = 7 };
var pointC = new Point(3, 7);
// 当 Y = 5 时为 X,否则为 Y
var result = pointB switch
{
(var first, 5) => first,
(_, var second) => second
};
// true
Console.WriteLine(pointB == pointC);
当然,record
是 immutable
的,并且是可以合并(继承)的,也可以标记为 sealed
或者 abstract
:
sealed data class Point3D(double X, double Y, double Z) : Point(X, Y);
上面的这种 record
声明方式是基于位置声明的,即 Point(first, second)
,fisrt
所代表的第一个位置将成为 X
,second
所代表的第二个位置将成为 Y
。
还有一种声明方式是基于名称的:
data class Point { double X; double Y };
var point = new Point { X = 5, Y = 6 };
Discriminated Unions
Discriminated unions 又叫做 enum class,这是一种全新的类型声明方式,顾名思义,是类型的 “枚举”。
例如,我们需要定义形状,形状有矩形、三角形和圆形,以前我们需要先编写一个 Shape
类,然后再创建 Rectangle
、Triangle
和 Circle
类继承 Shape
类,现在只需要几行就能完成,并且支持模式匹配和解构:
enum class Shape
{
Retangle(double Width, double Height);
Triangle(double Bottom, double Height);
Circle(double Radius);
}
然后我们就可以使用啦:
var circle = new Circle(5);
var rec = new Rectangle(3, 4);
if (rec is Retangle(_, 4))
{
Console.WriteLine("这不是我想要的矩形");
}
var height = GetHeight(rec);
double GetHeight(Shape shape)
=> shape switch
{
Retangle(_, height) => height,
Triangle(_, height) => height,
_ => throw new NotSupportedException()
};
Union and Intersection Types
当我们想要表示一个对象是两种类型其一时,将可以使用联合类型来表达:
public type SignedNumber = short | int | long | float | double | decimal;
public type ResultModel<T> = DataModel<T> | ErrorModel;
这在 Web API 中非常有用,当我们的接口可能返回错误的时候,我们不再需要将我们的数据用以下方式包含在一个统一的模式中:
public class ResultModel<T>
{
public string Message { get; set; }
public int Code { get; set; }
public T Data { get; set; }
}
我们将能够做到,不依赖异常等流程处理的方式做到错误时返回错误信息,请求正常处理时返回真实所需的数据:
public async ValueTask<DataModel | ErrorModel> SomeApi()
{
if (...) return new DataModel(...);
return new ErrorModel(...);
}
还有和类型,用来表示多个类型之和,我们此前在设计接口时,如果需要一个类型实现了多个接口,则需要定义一个新接口去实现之前的接口:
interface IA { ... }
interface IB { ... }
interface IAB : IA, IB { }
void Foo(IAB obj) { ... }
有了和类型之后,样板代码 IAB
将不再需要:
void Foo(IA & IB obj) { ... }
或者我们也可以这样声明新的类型:
type IAB = IA & IB;
Bottom Type
Bottom type 是一种特殊的类型 never
,never
类型是任何类型的子类,因此不存在该类型的子类。一个 never
类型的什么都不表示。
Union types 带来一个问题,就是我们有时候需要表达这个东西什么都不是,那么 never
将是一个非常合适的选择:
type Foo = Bar | Baz | never;
另外,never
还有一个重要的用途:控制代码流程,一个返回 never
的函数将结束调用者的逻辑,即这个函数不会返回:
void | never Foo(int x)
{
if (x > 5) return;
return never;
}
void Main()
{
Foo(6);
Console.WriteLine(1);
Foo(4);
Console.WriteLine(2);
}
上述代码将只会输出 1。
Concepts
Concepts 又叫做 type classes、traits,这个特性做到可以在不修改原有类型的基础上,为类型实现接口。
首先我们定义一个 concept
:
concept Monoid<T>
{
// 加函数
T Append(this T x, T y);
// 零属性
static T Zero { get; }
}
然后我们可以为这个 concept
创建类型类的实例:
instance IntMonoid : Monoid<int>
{
int Append(this int x, int y) => x + y;
static int Zero => 0;
}
这样我们就为 int
类型实现了 Monoid<int>
接口。
当我们想实现一个函数用来将一个 int
数组中的所有元素求和时,只需要:
public T Sum<T, inferred M>(T[] array) where M : Monoid<T>
{
T acc = M.Zero;
foreach (var i in array) acc = acc.Append(i);
return acc;
}
注意到,类型 M
会根据 T
进行自动推导得到 Monoid<int>
。
这样我们就能做到在不需要修改 int
的定义的情况下为其实现接口。
Higher Kinded Polymorphism
Higher kinded polymorphism,又叫做 templated template,或者 generics on generics,这是一种高阶的多态。
当我们需要表达一个类型是一个一阶泛型类型,且是实现了 ICollection<>
的容器之一时,我们可以写:
void Foo<T>() where T : <>, ICollection<>, new();
有了这个特性我们可以轻而易举的实现 monads
。
例如我们想要做一个将 IEnumerable<>
中所有元素变成某种集合类型的时候,例如 ToList()
等,我们就不需要显式地实现每一种需要的类型的情况(例如 List<>
):List<T> ToList(this IEnumerable<T> src)
了。
我们只需要这么写:
T<X> To<T, X>(this IEnumerable<X> xs) where T : <>, ICollection<>, new()
{
var result = new T<X>();
foreach (var x in xs) result.Add(x);
return result;
}
当我们想要把一个 IEnumerable<int> x
转换成 List<int>
时,我们只需简单的调用:x.To<List<>>()
即可。
Simple Programs
该特性允许编写 C# 代码时,无需 Main
函数,直接像写脚本一样直接在文件中编写逻辑代码,以此简化编写少量代码时却需要书写大量样板代码的问题:
以前写代码:
namespace Foo
{
class Bar
{
static async Task Main(string[] args)
{
await Task.Delay(1000);
Console.WriteLine("Hello world!");
}
}
}
现在写代码:
await Task.Delay(1000);
Console.WriteLine("Hello world!");
Expression Blocks
该特性允许创建表达式块:
Func<int, int, bool> greaterThan = (a, b) => if (a > b) a else b;
// true
greaterThan(5, 4);
因此有了以上特性,我们可以利用表达式实现更加复杂的东西。
后记
以上特性都是对代码布局和组成影响非常大的特性,并且不少特性几年前就已经被官方实现,但是因为存在尚未讨论解决的问题,迟迟没有发布进产品。
除此之外,还有几十个用于改进语言和方便用户使用等等的小特性也在未来的规划当中,此处不进行介绍。
未来的 C# 和今天的 C# 区别是很大的,作为一门多范式语言,C# 正在朝远离 Pure OOP 的方向渐行渐远,期待这门语言变得越来越好。