C语言完整特性详情

目录
  • C# 10 完整特性介绍
    • 1、record struct
    • 2、sealed record ToString 方法
    • 3、struct 无参构造函数
    • 4、匿名对象的 with
    • 5、全局的 using
    • 6、文件范围的 namespace
    • 7、常量字符串插值
    • 8、lambda 改进
      • 8.1. 支持 attributes
      • 8.2. 支持指定返回值类型
      • 8.3. 支持 ref 、in 、out 等修饰
      • 8.4. 头等函数
      • 8.5. 自然委托类型
    • 9、CallerArgumentExpression
    • 10、tuple 支持混合定义和使用
    • 11、接口支持抽象静态方法
    • 12、泛型 attribute
    • 13、允许在方法上指定 AsyncMethodBuilder
    • 14、line 指示器支持行列和范围
    • 15、嵌套属性模式匹配改进
    • 15改进的字符串插值

C# 10 完整特性介绍

前言:

C#使其拥有强如 Haskell 、Rust 的表达能力,不仅能提供从头到尾的跨程序集的静态类型支持,还能做到像动态类型语言那样的灵活。逻辑代码是类型的证明,只有类型系统强大了,代码编写起来才能更顺畅、更不容易出错。

1、record struct

首先自然是 record struct,解决了 record 只能给 class 而不能给 struct 用的问题:

record struct Point(int X, int Y);

用 record 定义 struct 的好处其实有很多,例如你无需重写 GetHashCode Equals 之类的方法了。

2、sealed record ToString 方法

之前 record 的 ToString 是不能修饰为 sealed 的,因此如果你继承了一个 record,相应的 ToString 行为也会被改变,因此这是个虚方法。

但是现在你可以把 record 里的 ToString 方法标记成 sealed,这样你的 ToString 方法就不会被重写了。

3、struct 无参构造函数

一直以来 struct 不支持无参构造函数,现在支持了:

struct Foo
{
    public int X;
    public Foo() { X = 1; }
}

但是使用的时候就要注意了,因为无参构造函数的存在使得 new struct() default(struct) 的语义不一样了,例如 new Foo().X == default(Foo).X 在上面这个例子中将会得出 false

4、匿名对象的 with

可以用 with 来根据已有的匿名对象创建新的匿名对象了:

var x = new { A = 1, B = 2 };
var y = x with { A = 3 };

这里 y.A 将会是 3 。

5、全局的 using

利用全局 using 可以给整个项目启用 usings,不再需要每个文件都写一份。比如你可以创建一个 Import.cs,然后里面写:

using System;
using i32 = System.Int32;

然后你整个项目都无需再 using System,并且可以用 i32 了。

6、文件范围的 namespace

这个比较简单,以前写 namespace 还得带一层大括号,以后如果一个文件里只有一个 namespace 的话,那直接在最上面这样写就行了:

namespace MyNamespace;

7、常量字符串插值

你可以给 const string 使用字符串插值了,非常方便:

const string x = "hello";
const string y = $"{x}, world!";

8、lambda 改进

这个改进可以说是非常大,我分多点介绍。

8.1. 支持 attributes

lambda 可以带 attribute 了:

f = [Foo] (x) => x; // 给 lambda 设置
f = [return: Foo] (x) => x; // 给 lambda 返回值设置
f = ([Foo] x) => x; // 给 lambda 参数设置

8.2. 支持指定返回值类型

此前 C# 的 lambda 返回值类型靠推导,C# 10 开始允许在参数列表最前面显示指定 lambda 类型了:

f = int () => 4;

8.3. 支持 ref 、in 、out 等修饰

f = ref int (ref int x) => ref x; // 返回一个参数的引用

8.4. 头等函数

函数可以隐式转换到 delegate,于是函数上升至头等函数:

void Foo() { Console.WriteLine("hello"); }
var x = Foo;
x(); // hello

8.5. 自然委托类型

lambda 现在会自动创建自然委托类型,于是不再需要写出类型了。

var f = () => 1; // Func<int>
var g = string (int x, string y) => $"{y}{x}"; // Func<int, string, string>
var h = "test".GetHashCode; // Func<int>

9、CallerArgumentExpression

现在,CallerArgumentExpression 这个 attribute 终于有用了。借助这个 attribute,编译器会自动填充调用参数的表达式字符串,例如:

void Foo(int value, [CallerArgumentExpression("value")] string? expression = null)
{
    Console.WriteLine(expression + " = " + value);
}

当你调用 Foo(4 + 5) 时,会输出 4 + 5 = 9。这对测试框架极其有用,因为你可以输出 assert 的原表达式了:

static void Assert(bool value, [CallerArgumentExpression("value")] string? expr = null)
{
    if (!value) throw new AssertFailureException(expr);
}

10、tuple 支持混合定义和使用

比如:

int y = 0;
(var x, y, var z) = (1, 2, 3);

于是 y 就变成 2 了,同时还创建了两个变量 x 和 z,分别是 1 和 3 。

11、接口支持抽象静态方法

这个特性将会在 .NET 6 作为 preview 特性放出,意味着默认是不启用的,需要设置 <LangVersion>preview</LangVersion> 和 <EnablePreviewFeatures>true</EnablePreviewFeatures>,然后引入一个官方的 nuget 包 System.Runtime.Experimental 来启用。

然后接口就可以声明抽象静态成员了,.NET 的类型系统正式具备虚静态方法分发能力。

例如,你想定义一个可加而且有零的接口 IMonoid<T>:

interface IMonoid<T> where T : IMonoid<T>
{
    abstract static T Zero { get; }
    abstract static T operator+(T l, T r);
}

然后可以对其进行实现,例如这里的 MyInt:

public class MyInt : IMonoid<MyInt>
{
    public MyInt(int val) { Value = val; }

    public static MyInt Zero { get; } = new MyInt(0);
    public static MyInt operator+(MyInt l, MyInt r) => new MyInt(l.Value + r.Value);

    public int Value { get; }
}

然后就能写出一个方法对 IMoniod<T> 进行求和了,这里为了方便写成扩展方法:

public static class IMonoidExtensions
{
    public static T Sum<T>(this IEnumerable<T> t) where T : IMonoid<T>
    {
        var result = T.Zero;
        foreach (var i in t) result += i;
        return result;
    }
}

最后调用:

List<MyInt> list = new() { new(1), new(2), new(3) };
Console.WriteLine(list.Sum().Value); // 6

你可能会问为什么要引入一个 System.Runtime.Experimental,因为这个包里面包含了 .NET 基础类型的改进:给所有的基础类型都实现了相应的接口,比如给数值类型都实现了 INumber<T>,给可以加的东西都实现了 IAdditionOperators<TLeft, TRight, TResult> 等等,用起来将会非常方便,比如你想写一个函数,这个函数用来把能相加的东西加起来:

T Add<T>(T left, T right) where T : IAdditionOperators<T, T, T>
{
    return left + right;
}

就搞定了。

接口的静态抽象方法支持和未来 C# 将会加入的 shape 特性是相辅相成的,届时 C# 将利用 interface 和 shape 支持 Haskell 的 class、Rust 的 trait 那样的 type classes,将类型系统上升到一个新的层次。

12、泛型 attribute

是的你没有看错,C# 的 attributes 支持泛型了,不过 .NET 6 中将以预览特定放出,因此需要 <LangVersion>preview</LangVersion>:

class TestAttribute<T> : Attribute
{
    public T Data { get; }
    public TestAttribute(T data) { Data = data; }
}

然后你就能这么用了:

[Test<int>(3)]
[Test<float>(4.5f)]
[Test<string>("hello")]

13、允许在方法上指定 AsyncMethodBuilder

C# 10 将允许方法上使用 [AsyncMethodBuilder(...)] 来使用你自己实现的 async method builder,代替自带的 Task 或者 ValueTask 的异步方法构造器。这也有助于你自己实现零开销的异步方法。

14、line 指示器支持行列和范围

以前 #line 只能用来指定一个文件中的某一行,现在可以指定行列和范围了,这对写编译器和代码生成器的人非常有用:

#line (startLine, startChar) - (endLine, endChar) charOffset "fileName"

// 比如 #line (1, 1) - (2, 2) 3 "test.cs"

15、嵌套属性模式匹配改进

以前在匹配嵌套属性的时候需要这么写:

if (a is { X: { Y: { Z: 4 } } }) { ... }

现在只需要简单的:

if (a is { X.Y.Z: 4 }) { ... }

就可以了

15改进的字符串插值

以前 C# 的字符串插值是很粗暴的 string.Format,并且对于值类型参数来说会直接装箱,对于多个参数而言还会因此而分配一个数组(比如 string.Format("{} {}", a, b) 其实是 string.Format("{} {}", new object [] { (object)a, (object)b })),这很影响性能。现在字符串插值被改进了:

var x = 1;
Console.WriteLine($"hello, {x}");

会被编译成:

int x = 1;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(7, 1);
defaultInterpolatedStringHandler.AppendLiteral("hello, ");
defaultInterpolatedStringHandler.AppendFormatted(x);
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());

上面这个 DefaultInterpolatedStringHandler 也可以借助 InterpolatedStringHandler 这个 attribute 替换成你自己实现的插值处理器,来决定要怎么进行插值。借助这些可以实现接近零开销的字符串插值。

Source Generator v2:

代码生成器在 C# 10 将会迎来 v2 版本,这个版本包含很多改进,包括强类型的代码构建器,以及增量编译的支持等等

到此这篇关于C# 10 完整特性详情的文章就介绍到这了,更多相关C# 10 完整特性内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言 联合(union)用法案例详解

    联合(union)的声明和结构与结构体类似,但是本质不同.    联合的所有成员引用的是内存中的相同位置.当你想在不同时刻把不同的东西存储于同一位置时,就可以使用联合.   构体(struct)中所有变量是"共存"的--优点是"有容乃大",全面:缺点是struct内存空间的分配是粗放的,不管用不用,全分配.   而联合体(union)中是各变量是"互斥"的--缺点就是不够"包容":但优点是内存使用更为精细灵活,也节省了内存空间

  • C语言入门篇--函数及数组用法

    目录 函数 1.函数的作用 2.函数的构成 (1)返回值 (2)函数名 (3)形参列表 (4)函数体 数组 1.定义数组 1.1不进行初始化 1.2进行初始化 1.3不给定数组元素个数 2.数组的经典用法 2.1求数组大小.元素大小.元素个数 2.2遍历 面向过程编程:C语言是面向过程的语言:在C语言中,把大部分的功能以一个个函数呈现,就称之为面向过程编程: 函数 是面向过程编程最关键的语法结构. 1.函数的作用 1.从工程上讲,函数可以让我们的代码更具有结构性,让代码更好看. 2.函数可以提升

  • C语言入门篇--局部全局变量的作用域及生命周期

    目录 1.变量的分类 1.1 局部变量 1.2 全局变量 1.3 知识点 1.3.1 就近原则 1.3.2 访问规则 1.3.3 有效范围 2.变量的使用 3.变量的作用域和生命周期 3.1 作用域 3.1.1 局部变量的作用域 3.1.2 全局变量的作用域 3.2 生命周期 3.2.1 局部变量的生命周期 3.2.2 全局变量的生命周期 1.变量的分类 1.1 局部变量 也称临时变量,在函数.代码块内定义,一般只可在代码块内部使用的变量. 1.2 全局变量 具有全局性,放在函数外,在同一___

  • C语言中bool变量的深入理解

    目录 前言 bool类型变量的大小 bool 值与0比较 c语言中bool如何输出 总结 前言 在一些高级语言当中,为了能够完成更好的逻辑判断,因此就有了bool类型,bool类型的变量值只有true和false两种. 而在C语言中,一般认为0为假,非0为真. 这是因为c99之前,c90是没有bool类型的的.但是c99引入了_Bool类型(_Bool就是一个类型,不过在新增头文件stdbool.h中,被重新用宏写成了 bool,为了保证C/C++兼容性). 目前为止大部分C语言书籍采用的标准还

  • C语言基于考研的栈和队列

    目录 栈 栈的基本操作 三角矩阵 总结 栈 栈的基本操作 InitStack(&S):初始化 StackEmpty(S):判空,空则true,非空则false Push(&S,x):入栈 Pop(&S,&x):出栈,并用x返回元素内容 GetTop(S,&x):读栈顶元素 DestroyStack(&S):销毁并释放空间 栈是一种受限的线性表,只允许在一端操作 栈若只能在栈顶操作,则只可能上溢 采用非递归方式重写递归时,不一定要用栈,比如菲波那切数列只要用循

  • C语言的数组与指针你了解吗

    目录 前言 一.数组的定义 二.数组空间的初始化 1. char数组赋值 2.char数组硬件开发规范 二.数组与指针 总结 前言 自学笔记,没有历史知识铺垫(省略百度部分)C语言数组的概念及使用 一.数组的定义 char a[n]; 注意:数组与指针非常相似 二者的区别: 数组为常量,约定禁止二次赋值,即数组不应该出现在=左侧,如:a="HelloWorld";数组在声明时,会申请一段连续的内存空间,指针不会数组元素为变量,标记可以修改指向任意内存(实际上会copy右侧变量/常量到左

  • 奇怪的C语言特性

    下面列出的特性未必奇怪,有的算是有趣. 1)a[2] 等价于 2[a] "aabbccdd"[5] 等价于 5["aabbccdd"] 这条特性可以用于使用数组.指针.字符串,但不能用在变量定义时.K&R C Programming language 217页对此有介绍. 2)二元.三元复合字符 http://en.wikipedia.org/wiki/Digraphs_and_trigraphs 字符串字面值??!将被认为是|,所以两个问号同时出现在字符串

  • C++在C语言基础之上增强的几个实用特性总结

    变量的定义 C语言中的变量都必须在作用域开始的位置定义!!  C++中更强调语言的"实用性",所有的变量都可以在需要使用时再定义. #include <iostream> using namespace std; int main11() { int i = 0; printf("ddd"); int k; // 这段代码在vc6,C语言编译情况下就会报错.就是因为这里的定义 system("pause"); return 0; }

  • 嵌入式项目使用C语言结构体位段特性实现断言宏校验数据范围有效性的方法

    关于位段的特性这里就不多说了,多去看看相应的C语言书籍都会有介绍了. 今天来介绍断言宏.什么是断言宏?断言宏可以认为是校验数据范围的有效性的一个宏的实现.我们来看看代码: #include <stdio.h> //结构体位段 #define CHECK(x) sizeof(struct {unsigned:(-!!(x));}) //检查常量是否在一定范围之内,如果不在范围之内,则编译报错 //比如定义一个0到1000的范围,如果传入的xxx小于0或者大于1000,则编译器发现会报错 #def

  • 整理C语言中各种类型指针的特性与用法

    指针为什么要区分类型: 在同一种编译器环境下,一个指针变量所占用的内存空间是固定的.比如,在16位编译器环境 下,任何一个指针变量都只占用8个字节,并不会随所指向变量的类型而改变. 虽然所有的指针都只占8个字节,但不同类型的变量却占不同的字节数. 一个int占用4个字节,一个char占用1个字节,而一个double占用8字节: 现在只有一个地址,我怎么才能知道要从这个地址开始向后访问多少个字节的存储空间呢,是4个,是1个,还是8个. 所以指针变量需要它所指向的数据类型告诉它要访问多少个字节存储空

随机推荐