乐趣区

关于c#:语法特性总结

C# 10 已与.NET 6、VS2022 一起公布,本文依照.NET 的公布程序,依据微软官网文档整顿 C# 中一些乏味的语法个性。

注:基于不同.NET 平台创立的我的项目,默认反对的 C#版本是不一样的。上面介绍的语法个性,会阐明引入 C#的版本,在应用过程中,须要留神应用 C#的版本是否反对对应的个性。C# 语言版本控制,可参考官网文档。

匿名函数

匿名函数是 C# 2 推出的性能,顾名思义,匿名函数只有办法体,没有名称。匿名函数应用 delegate 创立,可转换为委托。匿名函数不须要指定返回值类型,它会依据 return 语句主动判断返回值类型。

注:C# 3 后推出了 lambda 表达式,应用 lambda 能够以更简洁的形式创立匿名函数,应尽量应用 lambda 来创立匿名函数。与 lambda 不同的是,应用 delegate 创立匿名函数能够省略参数列表,可将其转换为具备任何参数列表的委托类型。

// 应用 delegate 关键字创立,无需指定返回值,可转换为委托,可省略参数列表(与 lambda 不同)Func<int, bool> func = delegate {return true;};

主动属性

从 C# 3 开始,当属性拜访器中不须要其它逻辑时,能够应用主动属性,以更简洁的形式申明属性。编译时,编译器会为其创立一个仅能够通过 get、set 拜访器拜访的公有、匿名字段。应用 VS 开发时,能够通过 snippet 代码片段 prop+ 2 次 tab 疾速生成主动属性。

// 属性老写法
private string _name;
public string Name
{get { return _name;}
    set {_name = value;}
}

// 主动属性
public string Name {get; set;}

另外,在 C# 6 当前,能够初始化主动属性:

public string Name {get; set;} = "Louzi";

匿名类型

匿名类型是 C# 3 后推出的性能,它无需显示定义类型,将一组只读属性封装到单个对象中。编译器会主动推断匿名类型的每个属性的类型,并生成类型名称。从 CLR 的角度看,匿名类型与其它援用类型没什么区别,匿名类型间接派生自 object。如果两个或多个匿名对象指定了程序、名称、类型雷同的属性,编译器会把它们视为雷同类型的实例。在创立匿名类型时,如果不指定成员名称,编译器会把用于初始化属性的名称作为属性名称。

匿名类型多用于 LINQ 查问的 select 查问表达式。匿名类型应用 new 与初始化列表创立:

// 应用 new 与初始化列表创立匿名类型
var person = new {Name = "Louzi", Age = 18};
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

// 用于 LINQ
var productQuery =
    from prod in products
    select new {prod.Color, prod.Price};

foreach (var v in productQuery)
{Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}

LINQ

C# 3 推出了杀手锏性能,查问表达式,即语言集成查问(LINQ)。查问表达式以查问语法示意查问,由一组相似 SQL 的语法编写的子句组成。

查问表达式必须以 from 子句结尾,必须以 select 或 group 子句结尾。在第一个 from 子句与最初一个 select 或 group 子句之间,能够蕴含:where、orderby、join、let、其它 from 子句等。

能够为 SQL 数据库、XML 文档、ADO.NET 数据集及实现了 IEnumerable 或 IEnumerable<T> 接口的汇合对象进行 LINQ 查问。

残缺的查问包含创立数据源、定义查问表达式、执行查问。查问表达式变量是存储查问而不是查问后果,只有在循环拜访查问变量后,才会执行查问。

可应用查问语法示意的任何查问都能够应用办法示意,倡议应用更易读的查问语法。有些查问操作(如 Count 或 Max)没有等效的查问表达式子句,必须应用办法调用。能够联合应用办法调用和查问语法。

对于 LINQ 的具体文档,参见微软官网文档

// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82};

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group

// Execute the query to produce the results
foreach (int testScore in scoreQuery)
{Console.WriteLine(testScore);
}

Lambda

C# 3 推出了很多弱小的性能,如主动属性、扩大办法、隐式类型、LINQ,以及 Lambda 表达式。

创立 Lambda 表达式,须要在 => 左侧指定输出参数(空括号指定零个参数,一个参数能够省略括号),右侧指定表达式或语句块(通常两三条语句)。任何 Lambda 表达式都能够转换为委托类型,表达式 Lambda 语句还能够转换为表达式树(语句 Lambda 不能够)。

匿名函数能够省略参数列表,Lambda 中不应用的参数能够应用弃元指定(C# 9)。

应用 async 和 await,能够创立蕴含异步解决的 Lambda 表达式和语句(C# 5)。

从 C# 10 开始,当编译器无奈推断返回类型时,能够在参数后面指定 Lambda 表达式的返回类型,此时参数必须加括号。

// Lambda 转换为委托
Func<int, int> square = x => x * x;
// Lambda 转换为表达式树
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
// 应用弃元指定不应用的参数
Func<int, int, int> constant = (_, _) => 42;
// 异步 Lambda
var lambdaAsync = async () => await JustDelayAsync();
Console.WriteLine($"main thread id: {Thread.CurrentThread.ManagedThreadId}");
lambdaAsync();

static async Task JustDelayAsync()
{await Task.Delay(1000);
    Console.WriteLine($"JustDelayAsync thread id: {Thread.CurrentThread.ManagedThreadId}");
}
// 指定返回类型,不指定返回类型会报错
var choose = object (bool b) => b ? 1 : "two";

扩大办法

扩大办法也是 C# 3 推出的性能,它可能向现有类型增加办法,且无需批改原始类型。扩大办法是一种静态方法,不过是通过实例对象语法进行调用,它的第一个参数指定办法操作的类型,用 this 润饰。编译器在编译为 IL 时会转换为静态方法的调用。

如果类型中具备与扩大办法雷同名称和签名的办法,则编译器会抉择类型中的办法。编译器进行办法调用时,会先在该类型的的实例办法中寻找,找不到再去搜寻该类型的扩大办法。

最常见的扩大办法是 LINQ,它将查问性能增加到现有的 System.Collections.IEnumerable 和 System.Collections.Generic.IEnumerable<T> 类型中。

为 struct 增加扩大办法时,因为是值传递,只能对 struct 对象的正本进行更改。从 C# 7.2 开始,能够为第一个参数增加 ref 润饰以进行援用传递,这样就能够对 struct 对象自身进行批改了。

static class MyExtensions
{public static void OutputStringExtension(this string s) => Console.WriteLine($"output: {s}");

    public static void OutputPointExtension(this Point p)
    {
        p.X = 10;
        p.Y = 10;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }

    public static void OutputPointWithRefExtension(ref this Point p)
    {
        p.X = 20;
        p.Y = 20;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }
}

// class 扩大办法
"Louzi".OutputStringExtension();

// struct 扩大办法
Point p = new Point(5, 5);
p.OutputPointExtension(); // output: (10, 10)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (5, 5)
p.OutputPointWithRefExtension();  // output: (20, 20)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (20, 20)

隐式类型(var)

从 C# 3 开始,在办法范畴内能够申明隐式类型变量(var)。隐式类型为强类型,由编译器决定类型。

var 罕用于调用构造函数创建对象实例时,从 C# 9 开始,这种场景也能够应用确定类型的 new 表达式:

// 隐式类型
var s = new List<int>();

// new 表达式
List<int> ss = new();

注:当返回匿名类型时,只能应用 var。

对象、汇合初始化列表

从 C# 3 开始,能够在单条语句中实例化对象或汇合并执行成员调配。

应用对象初始化列表,能够在创建对象时向对象的任何可拜访字段或属性调配值,能够指定结构函数参数或疏忽参数以及括号。

public class Person
{
    // 主动属性
    public int Age {get; set;}
    public string Name {get; set;}

    public Person() {}

    public Person(string name)
    {Name = name;}
}

var p1 = new Person {Age = 18, Name = "Louzi"};
var p2 = new Person("Sherilyn") {Age = 18};

从 C# 6 开始,对象初始化列表不仅能够初始化可拜访字段和属性,还能够设置索引器。

public class MyIntArray
{public int CurrentIndex { get; set;}

    public int[] data = new int[3];

    public int this[int index]
    {get => data[index];
        set => data[index] = value;
    }
}

var myArray = new MyIntArray {[0] = 1, [1] = 3, [2] = 5, CurrentIndex = 0 };

汇合初始化列表能够指定一个或多个初始值:

var persons = new List<Person>
{new Person { Age = 18, Name = "Louzi"},
    new Person {Age = 18, Name = "Sherilyn"}
};

内置泛型委托

.NET Framework 3.5/4.0,别离提供了内置的 Action 和 Func 泛型委托类型。void 返回类型的委托能够应用 Action 类型,Action 的变体最多有 16 个参数。有返回值类型的委托能够应用 Func 类型,Func 类型的变体最多同样 16 个参数,返回类型为 Func 申明中的最初一个类型参数。

Action<int> actionInstance = ActionInstance;
Func<int, string> funcInstance = FuncInstance;

static void ActionInstance(int n) => Console.WriteLine($"input: {n}");

static string FuncInstance(int n) => $"param: {n}";

dynamic

C# 4 次要的性能就是引入了 dynamic 关键字。dynamic 类型在变量应用及其成员援用时会绕过编译时类型查看,在运行时再进行解析。这便实现了与动静类型语言(如 JavaScript)相似的结构。

dynamic dyn = 1;
Console.WriteLine(dyn.GetType()); // output: System.Int32
dyn = dyn + 3; // 如果 dyn 是 object 类型,此句则会报错

命名参数与可选参数

C# 4 引入了命名参数和可选参数。命名参数可为形参指定实参,形式是指定匹配的实参加形参,这时无需匹配参数列表中的地位。可选参数通过指定参数默认值,能够省略实参。可选参数需位于参数列表开端,如果为一系列可选参数中的任意一个提供了实参,则必须为该参数后面的所有可选参数提供实参。

也能够应用 OptionalAttribute 个性申明可选参数,此时无需为形参提供默认值。

// 命名参数与可选参数
PrintPerson(age: 18, name: "Louzi");

// static void PrintPerson(string name, int age, [Optional, DefaultParameterValue("男")] string sex)
static void PrintPerson(string name, int age, string sex = "男") =>
    Console.WriteLine($"name: {name}, age: {age}, sex: {sex}");

动态导入

C# 6 中推出了动态导入性能,应用 using static 指令导入类型,能够无需指定类型名称即可拜访其动态成员和嵌套类型,这样防止了反复输出类型名称导致的艰涩代码。

using static System.Console;

WriteLine("Hello CSharp");

异样筛选器(when)

从 C# 6 开始,when 可用于 catch 语句中,用来指定为执行特定异样处理程序必须为 true 的条件表达式,当表达式为 false 时,则不会执行异样解决。

public static async Task<string> MakeRequest()
{var client = new HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try
    {
        var responseText = await streamTask;
        return responseText;
    }
    catch (HttpRequestException e) when (e.Message.Contains("301"))
    {return "Site Moved";}
    catch (HttpRequestException e) when (e.Message.Contains("404"))
    {return "Page Not Found";}
    catch (HttpRequestException e)
    {return e.Message;}
}

主动属性初始化表达式

C# 6 开始,能够为主动属性指定初始化值以应用类型默认值以外的值:

public class DefaultValueOfProperty
{public string MyProperty { get; set;} = "Property";
}

表达式体

从 C# 6 起,反对办法、运算符和只读属性的表达式体定义,自 C# 7.0 起,反对构造函数、终结器、属性、索引器的表达式体定义。

static void NewLine() => Console.WriteLine();

null 条件运算符

C# 6 起,推出了 null 条件运算符,仅当操作数的计算结果为非 null 时,null 条件运算符才会将成员拜访?. 或元素拜访?[]运算利用于其操作数;否则,将返回 null。

// null 条件表达式
public class ConditionalNull
{
    event EventHandler AEvent;

    public void RaiseAEvent() => AEvent?.Invoke(this, EventArgs.Empty);
}

内插字符串

从 C# 6 开始,能够应用 $ 在字符串中插入表达式,使代码可读性更高也升高了字符串拼接出错的概率。如果在内插字符串中蕴含大括号,需应用两个大括号(”{{“ 或 ””}}”)。如果内插表达式需应用条件运算符,须要将其放在括号内。从 C# 8 起,能够应用 $@”…” 或 @$”…” 模式的内插逐字字符串,在此之前的版本,必须应用 $@”…” 模式。

Console.WriteLine($"{name} is {age} year{(age == 1 ?"" : "s")} old.");

nameof

C# 6 提供了 nameof 表达式,nameof 可生成变量、类型或成员名称(非齐全限定)作为字符串常量。

public string Name
{
    get => name;
    set => name = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null");
}

out 改良

C# 7.0 中对 out 语法进行了改良,能够间接在办法调用的参数列表中申明 out 变量,无需再独自编写一条申明语句:

void Function(out int arg) {...}

// 未改良前
int n;
Function(out n);

// 改良后
Function(out int n);

元组

C# 7.0 中引入了对元组的语言反对(之前版本也有元组但效率低下),能够应用元组示意蕴含多个数据的简略构造,无需再专门写一个 class 或 struct。元组是值类型的,是蕴含多个公共字段以示意数据成员的轻量级数据结构,无奈为其定义方法。C# 7.3 后元组反对 == 与!=。

// 形式一,应用元组字段的默认名称:Item1、Item2、Item3 等
(string, string) unnamedLetters = ("a", "b");
Console.WriteLine($"{unnamedLetters.Item1}, {unnamedLetters.Item2}");
// 形式二
(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
// 形式三
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
// 形式四,C# 7.1 开始反对主动推断变量名称
int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // 元组元素名为 "count" 和 "label"

当某办法返回元组时,如需提取元组成员,可通过为元组的每个值申明独自的变量来实现,称为解构元组。应用元组作为办法返回类型,能够代替定义 out 办法参数。

// 解构元组
var (first, last) = Range(numbers);
Console.WriteLine($"{first} to {last}");

(int max, int min) = Range(numbers);
Console.WriteLine($"{min} to {max}");

弃元

从 C# 7.0 开始反对弃元,弃元是占位符变量,相当于未赋值的变量,示意不想应用该变量,应用下划线_示意弃元变量。如下列举了一些弃元的应用场景:

// 场景一:抛弃元组值
(_, _, area) = city.GetCityInformation(cityName);

// 场景二:从 C# 9 开始,能够抛弃 Lambda 表达式中的参数
Func<int, int, int> constant = (_, _) => 42;

// 场景三,抛弃 out 参数
DiscardsOut(out _);
static void DiscardsOut(out string s)
{
    s = "nothing";
    Console.WriteLine($"input is {s}");
}

模式匹配

C# 7.0 增加了模式匹配性能,之后每个次要 C# 版本都扩大了模式匹配性能。模式匹配用来测试表达式是否具备某些特色,is 表达式、switch 语句和 switch 表达式均反对模式匹配,可应用 when 关键字来指定模式的其余规定。

模式匹配目前蕴含这些类型:申明模式、类型模式、常量模式、关系模式、逻辑模式、属性模式、地位模式、var 模式、弃元模式,具体内容可参考官网文档。

is 模式表达式改良了 is 运算符性能,可在一条指令调配后果:

// is 模式匹配
if (input is int count) do somthing... ;

// 老写法
if (input is int)
{int count = (int)input;
    do somthing... ;
}

// is 模式进行空查看
string? message = "This is not the null string";
if (message is not null) Console.WriteLine(message);

default 文本表达式

默认值表达式生成类型的默认值,之前版本仅反对 default 运算符,C# 7.1 后加强了 default 表达式的性能,当编译器能够推断表达式类型时,能够应用 default 生成类型的默认值。

// 新写法
Func<string, bool> whereClause = default;
// 老写法
Func<string, bool> whereClause = default(Func<string, bool>);

switch 表达式

从 C# 8 开始,能够应用 switch 表达式。switch 表达式相较于 switch 语句的改良之处在于:

  • 变量在 switch 关键字之前;
  • 应用 => 替换 case : 构造;
  • 应用弃元_替换 default 运算符;
  • 应用表达式替换语句。
public enum Level
{
    One,
    Two,
    Three
}
public static int LevelToScore(Level level) => level switch
{
    Level.One   => 1,
    Level.Two   => 5,
    Level.Three => 10,
    _ => throw new ArgumentOutOfRangeException(nameof(level), $"Not expected level value: {level}"),
};

using 申明

C# 8 增加了 using 申明性能,它批示编译器申明的变量应在代码块的开端进行解决。using 申明相比传统的 using 语句代码更简洁,这两种写法都会使编译器在代码块开端调用 Dispose()。

static void WriteLinesToFile(IEnumerable<string> lines)
{using var file = new System.IO.StreamWriter("WriteLines.txt");
    do somthing... ;
    return;
    // file is disposed here
}

索引和范畴

C# 8 中增加了索引和范畴性能,为拜访序列中的单个元素或范畴提供了简洁的语法。该语法依赖两个新类型与两个新运算符:

  • System.Index 示意一个序列索引;
  • System.Range 示意序列的子范畴;
  • 开端运算符^,应用该运算符加数字,指定倒数第几个;
  • 范畴运算符..,指定范畴的开始和开端。

范畴运算符包含此范畴的开始,但不包含此范畴的开端。

var words = new string[]
{               // 失常索引             索引对应的开端运算符
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (words.Length)    ^0

Console.WriteLine($"The last word is {words[^1]}"); // dog
var allWords = words[..]; // 蕴含所有值,等同于 words[0..^0].
var firstPhrase = words[..4]; // 开始到 words[4],不蕴含 words[4]
var lastPhrase = words[6..]; // words[6]到开端
// 申明范畴变量
Range phrase = 1..4;
var text = words[phrase];

?? 与??=

?? 合并运算符:C# 6 后可用,如果左操作数的值不为 null,则?? 返回该值;否则,它会计算右操作数并返回其后果。如果左操作数的计算结果为非 null,则不会计算其右操作数。

??= 合并赋值运算符:C# 8 后可用,仅在左侧操作数的求值后果为 null 时,才将右操作数的值赋值给左操作数。否则,不会计算其右操作数。??= 运算符的左操作数必须是变量、属性或索引器元素。

// ?? 合并运算符
Console.WriteLine($"name is {OutputName(null)}");
static string OutputName(string name) => name ?? "some one";

// 应用??= 赋值运算符
variable ??= expression;

// 老写法
if (variable is null)
{variable = expression;}

顶级语句

C# 9 推出了顶级语句,它从应用程序中删除了不必要的流程,应用程序中只有一个文件可应用顶级语句。顶级语句使主程序更易读,缩小了不必要的模式:命名空间、class Program 和 static void Main()。

应用 VS 创立命令行我的项目,抉择.NET 5 及以上版本,就会应用顶级语句。

// 应用 VS2022 创立.NET 6.0 平台的命令行程序默认生成的内容
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

global using

C# 10 增加了 global using 指令,当关键字 global 呈现在 using 指令之前时,该 using 实用于整个我的项目,这样能够缩小每个文件 using 指令的行数。global using 指令能够呈现在任何源代码文件的结尾,但需增加在非全局 using 之前。

global 修饰符能够与 static 修饰符一起应用,也能够利用于 using 别名指令。在这两种状况下,指令的作用域都是以后编译中的所有文件。

global using System;
global using static System.Console; // 全局动态导入
global using Env = System.Environment; // 全局别名

文件范畴的命名空间

C# 10 引入了文件范畴的命名空间,可将命名空间蕴含为语句,后加分号且无需增加大括号。一个代码文件通常只蕴含一个命名空间,这样简化了代码且打消了一层嵌套。文件范畴的命名空间不能申明嵌套的命名空间或第二个文件范畴的命名空间,且它必须在申明任何类型之前,该文件内的所有类型都属于该命名空间。

using System;

namespace SampleFileScopedNamespace;

class SampleClass { }

interface ISampleInterface { }

struct SampleStruct { }

enum SampleEnum {a, b}

delegate void SampleDelegate(int i);

with 表达式

C# 9 开始引入了 with 表达式,它应用批改的特定属性和字段生成其操作对象的正本,未修改的值将保留与原对象雷同的值。对于援用类型成员,在复制操作数时仅复制对该成员实例的援用,with 表达式生成的正本和原对象都具备对同一援用类型实例的拜访权限。

在 C# 9 中,with 表达式的左操作数必须为 record 类型,C# 10 进行了改良,with 表达式的左操作数也能够是 struct 类型。

public record NamedPoint(string Name, int X, int Y);

var p1 = new NamedPoint("A", 0, 0);
var p2 = p1 with {Name = "B", X = 5};
退出移动版