【译】C#9的候选功能
通往C# 9 的漫長道路已經開始了,這是世界上第一篇關于C# 9候選功能的文章。閱讀完本文后,你將希望為將來遇到新的C#挑戰做好充分準備。 這篇文章基于:
C#語言版本計劃 9.0 候選功能
基于記錄和模式匹配的表達式
我一直在長時間等待這個功能。記錄是一種輕量級的不可變類型。它們是名義上的類型,可能有(方法、屬性、運算符等),并允許你比較結構相等。此外,在默認情況下,記錄屬性是只讀的。
記錄可以是值類型或者引用類型。
例如:
public class Point3D(double X, double Y, double Z); public class Demo { public void CreatePoint() { var p = new Point3D(1.0, 1.0, 1.0); } }上面的代碼轉換為:
public class Point3D
{
private readonly double <X>k__BackingField;
private readonly double <Y>k__BackingField;
private readonly double <Z>k__BackingField;
public double X {get {return <X>k__BackingField;}}
public double Y{get{return <Y>k__BackingField;}}
public double Z{get{return <Z>k__BackingField;}}
public Point3D(double X, double Y, double Z)
{
<X>k__BackingField = X;
<Y>k__BackingField = Y;
<Z>k__BackingField = Z;
}
public bool Equals(Point3D value)
{
return X == value.X && Y == value.Y && Z == value.Z;
}
public override bool Equals(object value)
{
Point3D value2;
return (value2 = (value as Point3D)) != null && Equals(value2);
}
public override int GetHashCode()
{
return ((1717635750 * -1521134295 + EqualityComparer<double>.Default.GetHashCode(X)) * -1521134295 + EqualityComparer<double>.Default.GetHashCode(Y)) * -1521134295 + EqualityComparer<double>.Default.GetHashCode(Z);
}
}
Using Records:
public class Demo
{
public void CreatePoint()
{
Point3D point3D = new Point3D(1.0, 1.0, 1.0);
}
}
新建議的特性“帶表達式”的記錄建議,你可以像下面這樣使用: varnewPoint3D=point3D.With(x:42);創建一個新的點(newPoint3D)就像一個已存在的點(point3D),但是X的值變為了42。
這種特性在模式匹配方面非常有效。我將在另一篇文章中介紹這個主題。
F#中記錄
從MSDN中的例子復制的F#代碼,類型 Point3D={X:float; Y:float; Z:float}
let evaluatePoint (point: Point3D) =
match point with
| { X = 0.0; Y = 0.0; Z = 0.0 } -> printfn "Point is at the origin."
| { X = xVal; Y = 0.0; Z = 0.0 } -> printfn "Point is on the x-axis. Value is %f." xVal
| { X = 0.0; Y = yVal; Z = 0.0 } -> printfn "Point is on the y-axis. Value is %f." yVal
| { X = 0.0; Y = 0.0; Z = zVal } -> printfn "Point is on the z-axis. Value is %f." zVal
| { X = xVal; Y = yVal; Z = zVal } -> printfn "Point is at (%f, %f, %f)." xVal yVal zVal
evaluatePoint { X = 0.0; Y = 0.0; Z = 0.0 }
evaluatePoint { X = 100.0; Y = 0.0; Z = 0.0 }
evaluatePoint { X = 10.0; Y = 0.0; Z = -1.0 }
這段代碼的輸出如下:
Point is at the origin.
Point is on the x-axis. Value is 100.000000.
Point is at(10.000000, 0.000000, -1.000000).
我想到的第一個問題是為什么我們需要記錄?使用結構不是更好嗎?
為回答這個問題,我從Reddit發表一個引用:
結構是需要一些準則來實現。你不必使他們不可變。不必實現值相等邏輯。不必使他們具有可比性。如果不這樣做,你將失去所有的便利,但是編譯器不會強制任何的這些約束
記錄類型由編譯器實現,這意味著你必須滿足所有的條件并且不能出現錯誤。
因此,他們不僅可以節省大量的樣板,還可以消除一大堆潛在bugs。
此外,這個功能在F#中已存在十多年,其他語言如(Scala,Kotlin)也有類似概念。
支持構造函數和記錄的其他語言示例:F#?
typeGreeter(name:string)=memberthis.SayHi()=printfn"Hi, %s"name
Scala
class Greeter(name: String)
{
def SayHi() = println("Hi, " + name)
}
Kotlin
class Greeter(val name: String)
{
fun sayhi()
{
println("Hi, ${name}");
}
}
同時,我們使用C#要編寫這么長的代碼,
public class Greeter
{
private readonly string _name;
public Greeter(string name)
{
_name = name;
}
public void Greet()
{
Console.WriteLine($ "Hello, {_name}");
}
}
當這個功能完成后,我們可以將C#代碼減少到,
public class Greeter(name: string)
{
public void Greet()
{
Console.WriteLine($ "Hello, {_name}");
}
}
更少的代碼!我喜歡它!
類型類(Type Classes)
此特性的靈感來自Haskell,她是我喜歡的功能。正如我之前在兩年前我的文章中所說,C#將實現更多函數式編程概念,這就是FP概念之一。在函數式編程中,類型類允許你在類型上添加一組操作,但是不能實現它。由于實現是在其他地方完成的,這是一種多態,但是比面向對象編程語言中的經典類更靈活或ad-hoc。
類型類和C#中的接口具有相似的用途,但是它們的工作方式有所不同,在某些情況下,類型類更多的是直接使用,因為它是直接在固定類中工作,而不是繼承層次結構的片段中。
此功能最初與“擴展所有內容”特性一起被引入,可以將它們組合在一起,如下面Mads Torgersen示例中所示。
我引用了官方提案中的一些文字:
一般來說,“形狀”聲明非常類似于接口聲明,除了它
幾乎可以定義任何類型的長遠(包括靜態成員)
可以通過擴展實現
只能在某些地方用作類型
Haskell 類型類例子:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
"Eq"為類名,而==,/=是類中的操作,類型“a”是類型“Eq”的實例
Haskell示例作為通用C#接口,
interface Eq <A>
{
bool Equal(A a, A b);
bool NotEqual(A a, A b);
}
Haskell示例作為C# 9 中的類型類(shape是類型類中一個新的獨特關鍵字)
shape Eq<A>
{
bool Equal(A a, A b);
bool NotEqual(A a, A b);
}
示例顯示接口和類型類直接的語法相似
interface Num<A>
{
A Add(A a, A b);
A Mult(A a, A b);
A Neg(A a);
}
struct NumInt : Num<int>
{
public int Add(int a, int b) => a + b;
public int Mult(int a, int b) => a * b;
public int Neg(int a) => -a;
}
使用C# 9 類型類
shape Num<A>
{
A Add(A a, A b);
A Mult(A a, A b);
A Neg(A a);
}
instance NumInt : Num<int>
{
int Add(int a, int b) => a + b;
int Mult(int a, int b) => a * b;
int Neg(int a) => -a;
}
Mads Torgersen 示例
重要信息:shape不是一個類型。相反,shape主要目的是用作通用約束,顯示類型參數以具有一個正確的shape。同時允許聲明的主體可以使用shape。
原始代碼
public shape SGroup<T>
{
static T operator +(T t1, T t2);
static T Zero {get;}
}
這個聲明說如果類型在T上實現了一個+運算符,那么它可以是SGroup,并且是一個零靜態屬性。
public extension IntGroup of int: SGroup<int>
{
public static int Zero => 0;
}
添加一個擴展:
public static AddAll<T>(T[] ts) where T: SGroup<T> // shape used as constraint
{
var result = T.Zero; // Making use of the shape's Zero property
foreach (var t in ts) { result += t; } // Making use of the shape's + operator
return result;
}
讓我們使用一些證書調用AddAll方法
int[] numbers = { 5, 1, 9, 2, 3, 10, 8, 4, 7, 6 };
WriteLine(AddAll(numbers)); // infers T = int
字典文字(Dictionary Literals)
引入更簡單的語法類創建初始化Dictionary對象,而無需指定Dictionary類型名稱或類型參數。Dictionary的類型參數使用用于數組類型推斷的現有規則確定。
// C# 1..8
var x = new Dictionary <string,int> () { { "foo", 4 }, { "bar", 5 }};
// C# 9
var x = ["foo":4, "bar": 5];
此提議是C#中的字典工作更簡單,并刪除冗余代碼。此外,值得一提的是,在F#和Swift等其他編程語言中也使用了類似的字典語法。
Params Span
到目前為止,在C#中不允許在結構聲明中使用no-arg構造函數和字段初始值設定項。在C# 9 中,將刪除此限制。StackOverflow example
public struct Rational
{
private long numerator;
private long denominator;
public Rational(long num, long denom)
{ /* Todo: Find GCD etc. */ }
public Rational(long num)
{
numerator = num;
denominator = 1;
}
public Rational() // This is not allowed
{
numerator = 0;
denominator = 1;
}
}
連接到 StackOverflow Example
來自官提案的引文,
HaloFour 提交于2017年9月6日 提案 #099 改提議旨在銷售阻止聲明默認構造函數的語言限制。CLR已經完全支持具有默認構造函數的結構體,并且C#支持使用它們。它們與常量完全無關,并且由于該特征已經存在于CLR基本且表現不同,因此無法與常量相關。
原生大小的數字類型
為本機引入一組新的本機類型(nint,nuint,nfloat,等)‘n’為原生。計劃為新數據類型的設計允許一個C#源文件使用32自然或64位存儲,具體取決于主機平臺類型和編輯設置。
本機類型取決于操作系統
nint nativeInt = 55; take 4 bytes when I compile in 32 Bit host.
nint nativeInt = 55; take 8 bytes when I compile in 64 Bit host with x64 compilation settings.
在xamarin中已存在類似概念。
xamarin 原生類型
固定大小的緩沖區
這些提供了一種通用且安全的機制,用于向C#語言聲明固定大小的緩沖區。
今天,用戶可以在不安全的環境中創建固定大小的緩沖區。然而,這需要用戶處理指針,手動執行邊界檢查,并且只支持一組有限的類型(bool,byte,char,short,int,long,sbyte,ushort,uint,ulong,float和double)。
此功能將使固定大小的緩沖區安全,如下示例所示
可以通過以下方式聲明一個安全的固定大小的緩沖區 publicfixedDXGI_RGBGammaCurve[1025];該聲明將編譯器轉化為內部表示,類似于以下內容
[FixedBuffer(typeof(DXGI_RGB), 1024)]
public ConsoleApp1.<Buffer>e__FixedBuffer_1024<DXGI_RGB> GammaCurve;
// Pack = 0 is the default packing and should result in indexable layout.
[CompilerGenerated, UnsafeValueType, StructLayout(LayoutKind.Sequential, Pack = 0)]
struct <Buffer>e__FixedBuffer_1024<T>
{
private T _e0;
private T _e1;
// _e2 ... _e1023
private T _e1024;
public ref T this[int index] => ref (uint)index <= 1024u ?
ref RefAdd<T>(ref _e0, index):
throw new IndexOutOfRange();
}
Uft8字符串文字
它是關于定義一種新類型的Uft8String,如 System.UTF8String myUTF8string="Test String";
based(T)
問題
interface I1
{
void M(int) { }
}
interface I2
{
void M(short) { }
}
interface I3
{
override void I1.M(int) { }
}
interface I4 : I3
{
void M2()
{
base(I3).M(0) // What does this do?
}
}
棘手的部分在于M(short)和M(int)都適用于M(0),但查找規則也說如果我們在再次派生的接口中找到使用的成員,我們忽略來自較少派生繼承接口的成員。結合在查找期間未找到覆蓋的規則,在查看I3是,我們發現第一件事是I2.M,這是適用的,這意味著I1.M 不會出現在使用成員列表中。 由于我們在上一次會議中得出結論,目標類型中必須存在一個實現,并且I2.M是唯一適用的成員,所寫的調用庫(I3).M(0)是一個錯誤,應為I2.M沒有在I3中的一個實現
更多信息:
https://github.com/dotnet/csharplang/issues/2337
https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-02-27.md
概要
你已經閱讀了第一個C# 9 的候選功能。正如你看到的,許多新功能受到其他編程語言或編程范例的啟發,而不是自我創新,但是好處是大多數候選功能在社區中得到了廣泛認可。
原文鏈接
注:翻譯原創
總結
以上是生活随笔為你收集整理的【译】C#9的候选功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎样学习和阅读技术书籍?
- 下一篇: AKS开讲啦! | DevOps wit