Essence: Summary of C#syntax characteristics

Essence: Summary of C#syntax characteristics

C#10 has been released together with. NET 6 and VS2022. This article follows the release order of. NET and sorts out some interesting syntax features in C#based on Microsoft's official documents.

最后更新 11/19/2021 5:38 PM
louzixl
预计阅读 24 分钟
分类
.NET
标签
.NET C#

C# 10 已与 .NET 6、VS2022 一起发布,本文按照.NET 的发布顺序,根据微软官方文档整理 C#中一些有趣的语法特性。

注:基于不同.NET 平台创建的项目,默认支持的 C#版本是不一样的。下面介绍的语法特性,会说明引入 C#的版本,在使用过程中,需要注意使用 C#的版本是否支持对应的特性。C#语言版本控制,可参考官方文档

anonymous function

Anonymous functions are a feature introduced by C#2. As the name suggests, anonymous functions only have method bodies and no names. Anonymous functions are created using delegate and can be converted to delegate. An anonymous function does not need to specify a return value type; it automatically determines the return value type based on the return statement.

** Note: Lambda expressions were introduced later in C#3. Using lambda can create anonymous functions in a more concise way. You should try to use lambda to create anonymous functions. Unlike lambda, using delegate to create anonymous functions can omit parameter lists and convert them to delegate types with any parameter lists. **

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

automatic properties

Starting with C#3, you can use automatic properties to declare properties in a more concise way when no other logic is needed in the property accessor. When compiling, the compiler creates a private, anonymous field for it that can only be accessed through the get and set accessors. When developing with VS, automatic attributes can be quickly generated through the sniper code fragment prop+2 tabs.

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

In addition, after C#6, automatic attributes can be initialized:

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

anonymous types

Anonymous types are a feature introduced after C#3. They do not need to display definition types and encapsulate a set of read-only properties into a single object. The compiler automatically infers the type of each attribute of the anonymous type and generates a type name. From the CLR's perspective, anonymous types are no different from other reference types. Anonymous types derive directly from object. If two or more anonymous objects specify attributes with the same order, name, and type, the compiler treats them as instances of the same type. When creating an anonymous type, if a member name is not specified, the compiler uses the name used to initialize the attribute as the attribute name.

Anonymous types are mostly used in select query expressions for LINQ queries. Anonymous types are created using new and initialization lists:

// 使用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 introduces the killer feature, query expressions, or Language Integrated Query (LINQ). A query expression represents a query in query syntax and consists of a set of clauses written in SQL-like syntax.

Query expressions must start with a from clause and end with a select or group clause. Between the first from clause and the last select or group clause, you can include: where, orderby, join, let, other from clauses, etc.

LINQ queries can be performed for SQL databases, XML documents, ADO. NET datasets, and collection objects that implement the IEnumerable or IEnumerable interface.

A complete query includes creating a data source, defining query expressions, and executing the query. Query expression variables store the query rather than the query results, and the query is executed only after looping through the query variables.

Any query that can be expressed using query syntax can be expressed using methods, and a more readable query syntax is recommended. Some query operations (such as Count or Max) do not have equivalent query expression clauses and must use method calls. You can use a combination of method invocation and query syntax.

关于 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 introduces many powerful features, such as automatic attributes, extended methods, implicit typing, LINQ, and Lambda expressions.

To create a Lambda expression, you need to specify the input parameters on the left side of =>(empty brackets specify zero parameters, and one parameter can be omitted), and the expression or statement block (usually two or three statements) on the right side. Any Lambda expression can be converted to a delegate type, and expression Lambda statements can also be converted to an expression tree (statement Lambda cannot).

Anonymous functions can omit parameter lists, and parameters that are not used in Lambda can be specified using discard elements (C#9).

Using async and await, you can create Lambda expressions and statements that include asynchronous processing (C#5).

Starting from C#10, when the compiler cannot infer the return type, it can specify the return type of a Lambda expression before the parameter, which must be bracketed.

// 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";

extension method

Extended methods is also a feature introduced by C#3, which allows you to add methods to existing types without modifying the original type. An extended method is a static method, but it is called through the instance object syntax, with its first parameter specifying the type of method action, decorated with this. The compiler converts calls to static methods when compiling to IL.

If the type has a method with the same name and signature as the extended method, the compiler selects the method in the type. When the compiler makes a method call, it will first look for the instance method of the type, and then search for the extension method of the type if it cannot find it.

The most common extension method is LINQ, which adds query capabilities to existing System.Collections.IEnumerable and System.Collections.Generic.IEnumerable types.

When adding an extension method to a struct, changes can only be made to a copy of the struct object because it is a value pass. Starting from C#7.2, you can add a ref decoration to the first parameter for reference passing, so that you can modify the struct object itself.

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)

Implicit type (var)

Starting from C#3, implicitly typed variables (var) can be declared within the scope of methods. Implicit types are strongly typed and are determined by the compiler.

var is often used when calling a constructor to create an object instance. Starting from C#9, this scenario can also use a new expression of a certain type:

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

** Note: When returning anonymous types, only var can be used. **

Object, collection initialization list

Starting with C#3, you can instantiate objects or collections and perform member assignments in a single statement.

Using an object initialization list, you can assign values to any accessible field or property of an object when the object is created, you can specify constructor parameters or ignore parameters, and parentheses.

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

Starting from C#6, object initialization lists can not only initialize accessible fields and properties, but also set indexers.

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

The collection initialization list can specify one or more initial values:

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

Built-in generic delegate

The. NET Framework 3.5/4.0 provides built-in Action and Func generic delegate types, respectively. Delegates of the void return type can use the Action type, and variants of Action have up to 16 parameters. Delegates with return value types can use the Func type. Variants of Func type have up to 16 parameters, and the return type is the last type parameter in the Func declaration.

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

The main function of C#4 is the introduction of the dynamic keyword. Dynamic types bypass compile-time type checking when variables and their member references are used, and are then resolved at run time. This implements constructs similar to dynamically typed languages such as JavaScript.

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

Named and optional parameters

C#4 introduces named and optional parameters. Named parameters can specify arguments for formal parameters by specifying matching practical parameters without matching positions in the parameter list. Optional Parameters You can omit arguments by specifying default values for the parameters. An optional parameter needs to be at the end of the parameter list. If an argument is provided for any of a series of optional parameters, you must provide arguments for all optional parameters preceding that parameter.

You can also declare optional parameters using the OptionalAttribute attribute, without providing default values for formal parameters.

// 命名参数与可选参数
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}");

static import

C#6 introduced a static import function. Using the using static directive to import types, you can access their static members and nested types without specifying the type name, which avoids obscure code caused by repeatedly entering type names.

using static System.Console;
WriteLine("Hello CSharp");

Exception Filter (when)

Starting from C#6, when can be used in catch statements to specify a conditional expression that must be true to execute a specific exception handler. When the expression is false, exception handling will not be executed.

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

Automatic property initialization expression

Starting with C#6, you can specify initialization values for automatic properties to use values other than the type default values:

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

expression body

Starting from C#6, expression body definitions of methods, operators, and read-only properties are supported. Starting from C#7.0, expression body definitions of constructors, finalists, properties, and indexers are supported.

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

null conditional operator

Since C#6, the null conditional operator has been introduced. Only when the evaluation result of the operand is non-null will the null conditional operator access a member?. Or element access? The [] operation is applied to its operand; otherwise, null is returned.

// null条件表达式
public class ConditionalNull
{
    event EventHandler AEvent;
    public void RaiseAEvent() => AEvent?.Invoke(this, EventArgs.Empty);
}

interpolated string

Starting from C#6, you can use $to insert expressions into strings, making the code more readable and reducing the probability of string splicing errors. If you include braces in the interpolated string, use two braces ("{{" or ""}"). If the interpolated expression requires a conditional operator, you need to place it in parentheses. Starting from C#8, interpolated verbatim strings of the form \(@"..." or @\)"..." can be used. Prior to this version, the form $@"..." must be used.

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

nameof

C#6 provides a nameof expression, which generates variables, types, or member names (not fully qualified) as string constants.

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

out improvement

The out syntax has been improved in C#7.0. You can directly declare the out variable in the parameter list of method calls without writing a separate declaration statement:

void Function(out int arg) { ... }
// 未改进前
int n;
Function(out n);
// 改进后
Function(out int n);

tuple

Language support for tuples has been introduced in C#7.0 (previous versions also had tuples but were inefficient). Tuples can be used to represent simple structures containing multiple data without having to write a class or struct specifically. Tuples are value-typed and lightweight data structures that contain multiple public fields to represent data members, for which methods cannot be defined. Tuples after C#7.3 support == and!=.

// 方式一,使用元组字段的默认名称: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"

When a method returns a tuple, if you need to extract tuple members, you can achieve this by declaring a separate variable for each value of the tuple, which is called deconstructing the tuple. Using tuples as the method return type is an alternative to defining out method parameters.

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

abandonment

Starting from C#7.0, discard variables are supported. Abandoned variables are placeholder variables, which are equivalent to unassigned variables, indicating that you do not want to use the variable. Use the underscore_to indicate discard variables. The following are some usage scenarios for discard elements:

// 场景一:丢弃元组值
(_, _, 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}");
}

pattern matching

C#7.0 added pattern matching functionality, and every major C#release since then expanded pattern matching functionality. Pattern matching is used to test whether an expression has certain characteristics. Is expressions, switch statements, and switch expressions all support pattern matching. You can use the when keyword to specify other rules for patterns.

Pattern matching currently includes these types: declaration pattern, type pattern, constant pattern, relational pattern, logical pattern, attribute pattern, location pattern, var pattern, and discard pattern. For details, please refer to the official document.

The is pattern expression improves the is operator functionality and allocates results in one instruction:

// 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 text expression

The default value expression generates the default value of the type. Previous versions only supported the default operator. After C#7.1, the function of the default expression has been enhanced. When the compiler can infer the expression type, default can be used to generate the default value of the type.

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

switch expression

Starting from C#8, switch expressions can be used. The improvements of switch expressions compared to switch statements are:

  • Variable before switch keyword;
  • Use => replace case : structure;
  • Replace the default operator with the discard_;
  • Replace statements with expressions.
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-declaration

C#8 adds a using declaration feature, which instructs the compiler to declare variables should be processed at the end of the code block. The using declaration is simpler than the traditional using statement code, and both of these will cause the compiler to call Dispose() at the end of the code block.

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

Index and scope

Index and range capabilities have been added to C#8 to provide concise syntax for accessing individual elements or ranges in a sequence. The syntax relies on two new types and two new operators:

  • System.Index represents a sequence index;
  • System.Range represents the sub-range of the sequence;
  • The end operator ^, which is used to add a number to specify the last;
  • Range operator.., Specifies the beginning and end of the range.

The range operator includes the beginning of the range, but not the end of the 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];

?? and??=

?? Merge operator: Available after C#6. If the value of the left operand is not null, then Returns that value; otherwise, it calculates the right-hand operand and returns its result. If the left operand is evaluated to be non-null, its right operand will not be evaluated.

??= Merge assignment operator: Available after C#8, the value of the right operand is assigned to the left operand only when the evaluation result of the left operand is null. Otherwise, its right operand will not be calculated.??= The left operand of an operator must be a variable, attribute, or indexer element.

// ??合并运算符
Console.WriteLine($"name is {OutputName(null)}");
static string OutputName(string name) => name ?? "some one";
// 使用??=赋值运算符
variable ??= expression;
// 老写法
if (variable is null)
{
    variable = expression;
}

top-level statement

C#9 introduces top-level statements, which remove unnecessary flow from applications where only one file can use top-level statements. Top-level statements make the main program easier to read and reduce unnecessary patterns: namespaces, class Program, and static void Main().

Use VS to create a command-line project, select. NET 5 and above, and top-level statements will be used.

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

global using

C#10 adds a global using directive. When the keyword global appears before the using directive, the using applies to the entire project, which reduces the number of lines used in each file. The global using directive can appear at the beginning of any source code file, but needs to be added before non-global using.

The global modifier can be used with the static modifier or applied to the using alias directive. In both cases, the scope of the directive is all files currently in compilation.

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

file-wide namespace

C#10 introduces file-wide namespaces, which can be included as statements followed by a semicolon and without the need to add curly braces. A code file usually contains only one namespace, which simplifies the code and eliminates a layer of nesting. A file-scoped namespace cannot declare a nested namespace or a second file-scoped namespace, and it must all types within the file belong to that namespace before declaring any type.

using System;
namespace SampleFileScopedNamespace;
class SampleClass { }
interface ISampleInterface { }
struct SampleStruct { }
enum SampleEnum { a, b }
delegate void SampleDelegate(int i);

with expression

C#9 initially introduced the with expression, which uses specific properties and fields modified to generate a copy of the object it operates on, and the unmodified values will retain the same value as the original object. For a member of a reference type, only the reference to the instance of the member is copied when copying the operand. Both the copy generated by the with expression and the original object have access to the same instance of the reference type.

In C#9, the left operand of the with expression must be of type record. C#10 has been improved, and the left operand of the with expression can also be of type 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 };
Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 4/22/2026

Support for. NET by operating system versions (250707 update)

Use virtual machines and test machines to test the support of each version of the operating system for. NET. After installing the operating system, it is passed by measuring the corresponding running time of the installation and being able to run the Stardust Agent.

继续阅读
同分类 / 同标签 2/7/2026

Summary of experience in using AOT

From the very beginning of project creation, you should develop a good habit of conducting AOT release testing in a timely manner whenever new features are added or newer syntax is used.

继续阅读