前言 如今 C 雖然發展到了 8.0 版本,引入了諸多的函數式特性,但其實在 C 未來的規劃當中,還有很多足以大規模影響現有 C 代碼結構和組成的特性,本文中將會對就重要的特性進行介紹,並用代碼示例展示這些特性。 以下特性將會在 C 9.0、10.0 或者更高版本提供。 Records Record ...
前言
如今 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);
Nothing;
}
然後我們就可以使用啦:
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()
};
利用此特性,我們可以輕而易舉的實現支持模式匹配的、type sound 的可空數據結構:
enum class Option<T>
{
Some(T value);
None;
}
var x = Some(5);
// Option<never>
var y = None;
void Foo(Option<T> value)
{
var bar = value switch
{
Some(var x) => x,
None => throw new NullReferenceException()
};
}
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 的方向漸行漸遠,期待這門語言變得越來越好。