【译】C#9的候选功能

通往C# 9 的漫长道路已经开始了,这是世界上第一篇关于C# 9候选功能的文章。阅读完本文后,你将希望为将来遇到新的C#挑战做好充分准备。
这篇文章基于:

基于记录和模式匹配的表达式

我一直在长时间等待这个功能。记录是一种轻量级的不可变类型。它们是名义上的类型,可能有(方法、属性、运算符等),并允许你比较结构相等。此外,在默认情况下,记录属性是只读的。

记录可以是值类型或者引用类型。

例如:

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);    
 }    
}  

新建议的特性“带表达式”的记录建议,你可以像下面这样使用:
var newPoint3D = 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#
type Greeter(name: string) = member this.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<TKey, TValue> 对象,而无需指定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中已存在类似概念。

固定大小的缓冲区

这些提供了一种通用且安全的机制,用于向C#语言声明固定大小的缓冲区。

今天,用户可以在不安全的环境中创建固定大小的缓冲区。然而,这需要用户处理指针,手动执行边界检查,并且只支持一组有限的类型(bool,byte,char,short,int,long,sbyte,ushort,uint,ulong,float和double)。

此功能将使固定大小的缓冲区安全,如下示例所示

可以通过以下方式声明一个安全的固定大小的缓冲区
public fixed DXGI_RGB GammaCurve[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中的一个实现

更多信息:

概要

你已经阅读了第一个C# 9 的候选功能。正如你看到的,许多新功能受到其他编程语言或编程范例的启发,而不是自我创新,但是好处是大多数候选功能在社区中得到了广泛认可。

原文链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值