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}");// 用于LINQvar 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 resultsforeach (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;// 异步Lambdavar 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.Int32dyn = 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) ^0Console.WriteLine($"The last word is {words[^1]}"); // dogvar 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 informationConsole.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 };