Loading AI tools
多範式(面向對象)編程語言 来自维基百科,自由的百科全书
C#是微软推出的一种基于.NET框架和后来的.NET的、面向对象的高级编程语言。C#衍伸自C和C++,继承了C和C++的强大功能,同时去掉了一些复杂特性,使其成为C语言家族中高效强大的编程语言。C#以.NET框架类库作为基础,拥有类似Visual Basic的快速开发能力。C#由安德斯·海尔斯伯格主持开发,微软在2000年发布了这种语言,希望借助这种语言来取代Java。C#已经成为Ecma国际和国际标准组织的标准规范。
编程范型 | 结构化、面向对象、泛型 |
---|---|
语言家族 | C |
设计者 | 微软 |
实现者 | 微软 |
发行时间 | 2000年 |
当前版本 |
|
操作系统 | Windows、Linux、Mac OS X 、 Android |
许可证 |
|
文件扩展名 | .cs , .csx |
网站 | docs |
主要实现产品 | |
.NET、.NET框架、Mono、DotGNU | |
派生副语言 | |
Cω、Spec#、Polyphonic C# | |
启发语言 | |
C++、Java、Eiffel、Modula-3、Object Pascal | |
影响语言 | |
Clojure[4]、D语言、F#、Java 5、Nemerle、Vala |
C#的发音为“C sharp”,“#”读作“sharp”(/ʃɑːp/),命名启发于音乐上的音名“C♯”,在音乐中“C♯”表示C升半音,为比C高一点的音节,且“#”形似4个加号,微软借助这样的命名,以表示C#在一些语言特性方面对C++的提升的意思。
由于显示器(标准字体、浏览器等)的技术限制,且大部分的键盘布局上不存在升记号(♯),所以井号(#)被用于此编程语言的名称中,约定在ECMA-334 C#语言规范中[5]。
ECMA标准列出的C#设计目标:
原Borland公司的首席研发设计师安德斯·海尔斯伯格(Anders Hejlsberg)在微软开发了Visual J++ 1.0,很快的Visual J++由1.1版本升级到6.0版。SUN公司认为Visual J++ 违反了Java开发平台的中立性,对微软提出了诉讼。2000年6月26日微软在奥兰多举行的“职业开发人员技术大会”(PDC 2000)上,发表新的语言C#。C#语言取代了Visual J++,语言本身深受Visual Basic、Java、C和C++ 的影响。
版本 | 语言规格 | 日期 | .NET框架版本 | Visual Studio的版本 | ||
---|---|---|---|---|---|---|
ECMA | ISO/IEC | Microsoft | ||||
C# 1.0 | 2002年12月(页面存档备份,存于互联网档案馆) | 2003年4月(页面存档备份,存于互联网档案馆) | 2002年1月(页面存档备份,存于互联网档案馆) | 2002年1月 | .NET Framework 1.0 | Visual Studio .NET 2002 |
C# 1.1 C# 1.2 |
2003年10月(页面存档备份,存于互联网档案馆) | 2003年4月 | .NET Framework 1.1 | Visual Studio .NET 2003 | ||
C# 2.0 | 2006年6月 | 2006年9月(页面存档备份,存于互联网档案馆) | 2005年9月(页面存档备份,存于互联网档案馆) | 2005年11月 | .NET Framework 2.0 | Visual Studio 2005 |
C# 3.0 | 否 | 2007年8月(页面存档备份,存于互联网档案馆) | 2007年11月 |
.NET Framework 2.0 (Except LINQ)[6] |
Visual Studio 2008 Visual Studio 2010 | |
C# 4.0 | 2010年4月 | 2010年4月 | .NET Framework 4 | Visual Studio 2010 | ||
C# 5.0 | 2017年12月(页面存档备份,存于互联网档案馆) | 2018年12月(页面存档备份,存于互联网档案馆) | 2013年6月(页面存档备份,存于互联网档案馆) | 2012年8月 | .NET Framework 4.5 | Visual Studio 2012 Visual Studio 2013 |
C# 6.0 | 否 | 草案(页面存档备份,存于互联网档案馆) | 2015年7月/2016-06-27 | .NET Framework 4.6/.NET Core 1.0 | Visual Studio 2015 | |
C# 7.0 | 建议草案 (页面存档备份,存于互联网档案馆) | 2017年3月 | .NET Framework 4.6.2 | Visual Studio 2017 | ||
C# 7.1 | 否 | 否 | 建议草案 (页面存档备份,存于互联网档案馆) | 2017年8月/2016-08-14 | .NET Framework 4.7/.NET Core 2.0 | Visual Studio 2017 version 15.3[7] |
C# 7.2 | 否 | 否 | 建议草案 | 2017年11月 | .NET Framework 4.7.1 | Visual Studio 2017 version 15.5[8] |
C# 7.3 | 否 | 否 | 建议草案 (页面存档备份,存于互联网档案馆) | 2018年5月/2018-05-30/2018-12-04 | .NET Framework 4.7.2/.NET Core 2.1/.NET Core 2.2 | Visual Studio 2017 version 15.7[8] |
C# 8 | 否 | 否 | 建议草案 (页面存档备份,存于互联网档案馆) | 2019年9月/2019-09-23/2019-12-03 | .NET Framework 4.8/.NET Core 3.0/.NET Core 3.1 | Visual Studio 2019 version 16.3[8] |
C# 9 | 否 | 否 | 建议草案 (页面存档备份,存于互联网档案馆) | 2020年11月 | .NET 5 | Visual Studio 2019 version 16.8[8] |
C# 10[9] | 否 | 否 | 建议草案 (页面存档备份,存于互联网档案馆) | 2021年11月 | .NET 6 | Visual Studio 2022 version 17.0[10] |
C# 11[9] | 否 | 否 | 建议草案 (页面存档备份,存于互联网档案馆) | 2022年11月 | .NET 7 | Visual Studio 2022 version 17.4[11] |
C# 12[12] | 否 | 否 | 建议草案 | 2023年11月 | .NET 8 | Visual Studio 2022 version 17.8[13] |
C# 13[14] | 否 | 否 | 建议草案 | 2024年9月 | .NET 9 | Visual Studio 2022 version 17.12[15] |
针对于.NET SDK 2.0(相对应于ECMA-334标准第三版),C# 的新特性有:
分部类别将类别的实现分在多个文件中。该概念于C# 中首次出现,除了能将一个类别的成员分开存放,还使ASP.NET中的代码后置得以实现。代码后置实现了HTML代码和后台交互代码的分离。
file1.cs:
public partial class MyClass1
{
public void MyMethod1()
{
// implementation
}
}
file2.cs:
public partial class MyClass1
{
public void MyMethod2()
{
// implementation
}
}
分部类别这个特性允许将一个类别的编写工作分配给多个人,一人写一个文件,便于版本控制。它又可以隔离自动生成的代码和人工书写的代码,例如设计窗体应用程序时。
泛型,或参数化类型,是被C#支持的.NET 2.0特性。不同于C++模版,.NET参数化类型是在运行时被实例化,而不是编译时,因此它可以跨语言,而C++模版却不行。C#泛型类在编译时,先生成中间代码IL,通用类型符号T只是一个占位符;在实例化类时,根据实际数据类型代替T并由即时编译器(JIT)生成本地代码,其中使用了实际的数据类型,等同于用实际类型写的普通的类。
它支持的一些特性并不被C++模版直接支持,比如约束泛型参数实现一个接口。另一方面,C# 不支持无类型的泛型参数。不像Java中的泛型,在CLI虚拟机中,.NET generics使用具化生成泛型参数,它允许优化和保存类型信息。[16]
泛型类中,可以用where关键字对参数类型实现约束。例如:
class Node<T, V>
where T : Stack, IComparable, new(), class
where V : Stack, struct
{...}
上述表示T和V必须是Stack类或其派生类,T必须继承了IComparable接口、有无参构造函数、是引用类型;V必须是值类型。
泛型不仅能作用在类上,也可单独用在类的方法上,称为“泛型方法”。
泛型类的静态成员变量在相同封闭类间共享,不同的封闭类间不共享。
泛型类中的方法重载,参数类型T和V在运行时确定,不影响这个类通过编译。C#的泛型是在实例的方法被调用时检查重载是否产生混淆,而不是在泛型类本身编译时检查。特别地,当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。
静态类别它不能被实例化,并且只能有静态成员。这同很多过程语言中的模块概念相类似。
一种新形式的迭代器它提供了函数式编程中的generator,使用yield return
类似于Python中使用的yield
// Method that takes an iterable input (possibly an array)
// and returns all even numbers.
public static IEnumerable<int> GetEven(IEnumerable<int> numbers)
{
foreach (int i in numbers)
{
if (i % 2 == 0) yield return i;
}
}
注意事项:
匿名方法类似于函数式编程中的闭包。[17]匿名方法是通过使用 delegate 关键字创建委托实例来声明的。例如:
delegate void NumberChanger(int n);
NumberChanger nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};
public void Foo(object parameter)
{
// ...
ThreadPool.QueueUserWorkItem(delegate
{
// anonymous delegates have full access to local variables of the enclosing method
if(parameter == ...)
{
// ...
}
// ...
});
}
例子:
string status = string.Empty;
public string Status
{
get { return status; } // anyone can get value of this property,
protected set { status = value; } // but only derived classes can change it
}
可空类型(跟个问号,如int? i = null;
)允许设置null
给任何类类型。
int? i = null;
object o = i;
if(o == null)
Console.WriteLine("Correct behaviour - runtime version from September 2005 or later");
else
Console.WriteLine("Incorrect behaviour - pre-release runtime (from before September 2005)");
(??
):如果左运算数表达式的值不为空值时回传该值,如果为空值则返回右运算数表达式的值。
object nullObj = null;
object obj = new Object();
return nullObj ?? obj; // returns obj
主要用作将一个可空类型赋值给不可空类型的简便语法
int? i = null;
int j = i ?? 0; // Unless i is null, initialize j to i. Else (if i is null), initialize j to 0.
C# 3.0发布于2007年10月17日,是.NET Framework 3.5的一部分,它的新特性灵感来自于函数式编程语言,如:Haskell和ML,并广泛地引入了Language Integrated Query(LINQ)模式到通用语言运行库中e.[19]
语言集成查询(英语:Language Integrated Query,缩写:LINQ):[20] 上下文相关关键字"from
, where
, select
"可用于查询SQL、XML、集合等。这些标识符在LINQ上下文中被作为关键字,但是它们的增加不会破坏原有的名为from
、where
或select
的变量。
Customer c = new Customer();
c.Name = "James";
可写作:
Customer c = new Customer() { Name = "James" };
MyList list = new MyList();
list.Add(1);
list.Add(2);
可写作
MyList list = new MyList { 1, 2 };
假设MyList
实现了System.Collections.IEnumerable
且有一个Add
方法method[21]
var x = new { Name = "James" };
局部变量类型推断:
var x = new Dictionary<string, List<float>>();
等同于
Dictionary<string, List<float>> x = new Dictionary<string, List<float>>();
它只是一个语法糖,这个特性被匿名类型声明时所需要
Lambda表达式(无函数名称的对象方法在编程语言中的表达语法):
listOfFoo.Where(
delegate(Foo x)
{
return x.Size > 10;
}
)
listOfFoo.Where(x => x.Size > 10);
编译器翻译Lambda表达式为强类型委托或强类型表达式树。
注意事项:
编译器将自动生成私有变量和适当的getter(get访问器)和setter(set访问器),如:
public string Name
{
get;
set;
}
扩展方法能够使现有的类型添加方法,而无需创建新的派生类型、重新编译或以其它方式修改原始类型。
使用拓展方法,必须在一个非嵌套、非泛型的静态类中定义一个静态方法,方法第一个参数必须附加this关键字作为前缀,第一个参数不能有其它修饰符(如ref或者out),这个方法将被编译器添加到该this的类型中。
public static class IntExtensions
{
public static void PrintPlusOne(this int x)
{
Console.WriteLine(x + 1);
}
}
int foo = 0;
foo.PrintPlusOne();
注意事项:
允许代码生成器生成方法声明作为扩展点,如果有人在另一个部分类实现了它才会被包含于原代码编译。[22]
例子:
partial class C
{
static partial void M(int i); // defining declaration
}
partial class C
{
static partial void M(int i)
{
dosomething();
}
}
C# 4.0新增dynamic关键字,提供动态编程(dynamic programming),把既有的静态对象标记为动态对象,类似javascript, Python或Ruby。
dynamic关键字标记的实例被处理成一个特殊包装的object对象,取消了CLI的编译时类型检查,编译时被假定支持任何操作,但如果并不实际支持则运行时报错。
dynamic calc = GetCalculator();
int sum = calc.Add(10, 20);
public StreamReader OpenFile(string path, int bufferSize = 1024)
{ ... }
调用OpenFile时,顺序可以完全颠倒:
OpenFile(bufferSize: 4096, path: "foo.txt");
在C#中打开一个Word文件:
static void Main(string[] args)
{
Word.Application wordApplication = new Word.Application() { Visible = true };
wordApplication.Documents.Open(@"C:\plant.docx", ReadOnly: true);
}
在C#中指定Excel的某一格文字:
excelObj.Cells[5, 5].Value = "This is sample text";
C# 4.0支持协变和逆变,例如在泛型接口可以加上in、out关键字。
public interface IComparer<in T>
{
int Compare(T left, T right);
}
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
using System;
public class Person
{
public Person(string firstName, string lastName)
{
fname = firstName;
lname = lastName;
}
private string fname;
private string lname;
public override string ToString() => $"{fname} {lname}".Trim(); //返回值类型string
public void DisplayName() => Console.WriteLine(ToString()); //返回值类型void
public string Name => $"{fname} {lname}".Trim();//只读属性
}
能够直接宣告一个变量在它要传入的地方,当成一个 out 的引数[23]
元组/对象的解构:
var tuple = (1, 2, 3, 4, 5);
(_, _, _, _, var fifth) = tuple;
使用 is/switch 的模式匹配:
var obj = CultureInfo.CurrentCulture.DateTimeFormat;
switch (obj)
{
case IFormatProvider fmt:
Console.WriteLine($"{fmt} object");
break;
case null:
Console.Write("A null object reference");
break;
case object _:
Console.WriteLine("Some object type without format information");
break;
}
if (obj is object _) { ... }
对具有 out 参数的方法的调用:
var point = new Point(10, 10);
// 只要 x, 不关心 y
point.GetCoordinates(out int x, out _);
作用域内独立使用场景:
void Test(Dto dto)
{
_ = dto ?? throw new ArgumentNullException(nameof(dto));
}
using System;
public class Location
{
private string locationName;
public Location(string name) => Name = name; //构造函数
public string Name
{
get => locationName; //get属性
set => locationName = value; //set属性
}
public override string ToString() => GetType().Name;
~Location() => Console.WriteLine($"The {ToString()} finalizer is executing."); //析构函数
private string[] types = { "Baseball", "Basketball", "Football",
"Hockey", "Soccer", "Tennis",
"Volleyball" };
public string this[int i]
{
get => types[i]; //索引器
set => types[i] = value;
}
}
记录类型, 是一种引用类型, 默认是不可变的。 记录类型的相等判断可以通过引用或者结构进行判断的。
// 默认不可变的记录类型
public record Person(string Name, int Age);
// 可变记录类型
public record MutablePerson(string Name, int Age)
{
public string Name { get; set; } = Name;
public int Age { get; set; } = Age;
}
var person1 = new Person("Alice", 40);
var person2 = new Person("Alice", 40);
Console.WriteLine(person1 == person2); // True 结构相同
Console.WriteLine(person1.Equals(person2)); // True 结构相同
Console.WriteLine(ReferenceEquals(person1, person2)); // False, 引用不同
// 改变默认的记录! --> 创建一个新的记录。
var person3 = person1 with { Age = 43 };
Console.WriteLine(person3 == person1); // False 结构不同
// 解构 (Destruct) 一个记录, 将记录的属性提取为本地变量
var (name, age) = person3;
var person4 = new MutablePerson("Alice", 40);
person4.Age = 43;
// 记录类型也可以被继承
public record Citizen(string Name, int Age, string Country) : Person(Name, Age);
var citizen = new Citizen("Alice", 40, "China");
Console.WriteLine(person1 == citizen); // False 类型不同;
init访问子表示该属性所属类型仅能在构造函数(Constructor)中或是属性初始化式子中赋予其值,如果尝试在其他地方设置该属性的值,在编译时便会遭编译器阻止。
示例如下:在这个示例中,建立了一个Student
类型,并且属性StudentName
与StudentID
只能在初始化时赋予其值。
public class Student
{
public Student()
{
}
public Student(string studentName,string studentID)
{
StudentName = studentName;
StudentID = studentID;
}
public string StudentName { get; init; } = "Default Name";
public string StudentID { get; init; } = "00000000";
}
如果在此时撰写以下代码:
Student DemoStudent = new Student();
DemoStudent.StudentName = "Test Name";
编译器便会无法编译并且掷回错误。
而如果要建立学生名称为“Test Name”,学生ID为“0001”的学生,则需要写成:
Student DemoStudent = new Student() //物件初始化運算式
{
StudentName = "Test Name";
StudentID = "0001"
};
或是
Student DemoStudent = new Student("Test Name","0001"); //藉由類型的建構式初始化StudentName以及StudentID。
在以前的版本,开发者在撰写最上层语句(如Program.cs)代码时,需要包含完整的namespace与class架构,因此如果要撰写Hello World程序时,代码就会是:
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
但是在C# 9之后,最上层语句的代码不需要包含namespace以及class,可将其简化为:
using System;
Console.WriteLine("Hello World!");
//或者简化为一行语句:
System.Console.WriteLine("Hello World!");
注意, 一个程序中, 只能有一个文件使用顶级语句, 并且顶级语句必须位于命名空间或类型定义之前。
Func<int, int, int> zero = (_, _) => 0;
Func<int, int, int> func = delegate (int _, int _) { return 0; };
在 C# 9 之前,即便不使用的 Lambda 参数也需要给它命名。C# 9 支持弃元参数一方面简化了命名,另一方面也节省了内存分配。更重要的是它使得编程的意图更明确,让人一看就知道这个参数是不用的,增强了代码的可读性和可维护性。
Init only setters,只能通过对象初始化进行赋值的属性。
public class InitDemo
{
public string Start { get; init; }
public string Stop { get; init; }
}
// initDemo.Start = "Now"; // Error
// initDemo.End = "Tomorrow"; // Error
var initDemo = new InitDemo
{
Start = "Now",
Stop = "Tomorrow"
};
使用 delegate* 可以声明函数指针。
unsafe class FunctionPointer {
static int GetLength(string s) => s.Length;
delegate*<string, int> functionPointer = &GetLength;
}
public void Test() {
Console.WriteLine(functionPointer("test")); // 4;
}
[System.Runtime.CompilerServices.SkipLocalsInit]
static unsafe void DemoLocalsInit() {
int x;
// 注意, x 没有初始化, 输出结果不确定;
Console.WriteLine(*&x);
}
两个新的整数类型 nint 和 nunit , 依赖宿主机以及编译设定。
协变返回类型为重写方法的返回类型提供了灵活性。覆盖方法可以返回从被覆盖的基础方法的返回类型派生的类型。
class Person
{
public virtual Person GetPerson() { return new Person(); }
}
class Student : Person
{
public override Student GetPerson() { return new Student(); }
}
ModuleInitializerAttribute 为组件 (assembly) 定义初始化代码, 当初始化/加载时执行, 可以类比类的静态构造函数, 但是是组件级别的。
static 修饰符添加到 lambda 表达式或匿名方法 。这将无法捕获局部变量或实例状态,从而防止意外捕获其他变量。
移除了分部方法的下述限制:
如果创建对象的类型已知时,可以在new表达式中省略该类型。
Point p = new(1, 1);
Dictionary<string, int> dict = new();
Point[] points = { new(1, 1), new (2, 2), new (3, 3) };
var list = new List<Point> { new(1, 1), new(2, 2), new(3, 3)};
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace CoreApp2
{
class Program
{
static void Main(string[] args)
{
[Conditional("DEBUG")]
static void DoSomething([NotNull] string test)
{
System.Console.WriteLine("Do it!");
}
DoSomething("Doing!");
}
}
}
可以为任意类型添加一个 GetEnumerator 扩展方法, 返回一个 IEnumerator 或者 IAsyncEnumerator 实例, 从而在 foreach 循环中使用。
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace CoreApp2
{
public static class Extensions
{
public static IEnumerator<T> GetEnumerator<T>(this IEnumerator<T> enumerator) => enumerator;
}
class Program
{
static void Main(string[] args)
{
IEnumerator<string> enumerator = new Collection<string> {"A", "B", "C"}.GetEnumerator();
foreach (var item in enumerator)
{
Console.WriteLine(item);
}
}
}
}
Type patterns 类型匹配,判断一个变量的类型
object obj = new int();
var type = obj switch
{
string => "string",
int => "int",
_ => "obj"
};
Console.WriteLine(type); // int
Relational patterns 关系匹配:
class Person
{
public string name;
public int age;
public Person(string a, int b) { name = a;age = b; }
public void Deconstruct(out string a,out int b){a = name;b = age; }
}
class Program
{
static void Main(string[] args)
{
var person1 = new Person("Alice", 40);
var inRange = person1 switch
{
(_, < 18) => "less than 18",
(_, > 18) => "greater than 18",
(_, 18) => "18 years old!"
};
Console.WriteLine(inRange); // greater than 18
}
}
Conjunctive and patterns 逻辑合取匹配:
// And pattern
var person1 = new Person("Alice", 40);
var ageInRange = person1 switch
{
(_, < 18) => "less than 18",
("Zhang Zhimin", _) and (_, >= 18) => "Alice is greater than 18"
};
Console.WriteLine(ageInRange); // Alice is greater than 18
Disjunctive or patterns 逻辑析取匹配:
// Or pattern
var person1 = new Person("Alice", 40);
var ageInRange = person1 switch
{
(_, < 18) => "less than 18",
(_, 18) or (_, > 18) => "18 or greater"
};
Console.WriteLine(ageInRange); // 18 or greater
Negated not patterns 逻辑非匹配
// Not pattern
var person1 = new Person("Alice", 40);
var meOrNot = person1 switch
{
not ("Alice", 40) => "Not me!",
_ => "Me :-)"
};
Console.WriteLine(meOrNot); // Me :-)
Parenthesized patterns 带括号的优先级匹配:
// Parenthesized patterns
var is10 = new IsNumber(true, 10);
var n10 = is10 switch
{
((_, > 1 and < 5) and (_, > 5 and < 9)) or (_, 10) => "10",
_ => "not 10"
};
Console.WriteLine(n10); // 10
解决了 record 只能给 class 而不能给 struct 用的问题:
record struct Point(int X, int Y);
可以把 record 里的 ToString 方法标记成 sealed
无参构造函数使得new struct() 和 default(struct) 的语义不一样
var x = new { A = 1, B = 2 };
var y = x with { A = 3 };
这里 y.A 将会是 3 。
可以给整个项目启用 using,不需要每个文件都写一份。
以前写 namespace 还得带一层大括号。现在如果一个文件里只有一个 namespace 的话,直接在文件开头写:namespace MyNamespace;
const string x = "hello";
const string y = $"{x}, world!";
f = [Foo] (x) => x; // 给 lambda 设置
f = [return: Foo] (x) => x; // 给 lambda 返回值设置
f = ([Foo] x) => x; // 给 lambda 参数设置
此前 C# 的 lambda 返回值类型靠推导,C# 10允许在参数列表之前显式指定 lambda 返回值类型:
f = int () => 4;
f = ref int (ref int x) => ref x; // 返回一个参数的引用
函数可以隐式转换到 delegate,于是函数上升为头等函数(first function):
void Foo() { Console.WriteLine("hello"); }
var x = Foo;
x(); // hello
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>
使用CallerArgumentExpression这个attribute,编译器会自动填充调用参数的表达式字符串,例如:
void Foo(int value, [CallerArgumentExpression("value")] string? expression = null)
{
Console.WriteLine(expression + " = " + value);
}
当你调用 Foo(4 + 5) 时,会输出 4 + 5 = 9。这对测试框架极其有用
int y = 0;
(var x, y, var z) = (1, 2, 3);
于是 y 就变成 2 了,同时还创建了两个变量 x 和 z,分别是 1 和 3 。
.NET 6中这个特性为preview特性。
在方法上用 [AsyncMethodBuilder(...)],来使用自己实现的 async method builder,代替自带的 Task 或者 ValueTask 的异步方法构造器。有助于实现零开销的异步方法。
以前 #line 只能用来指定一个文件中的某一行,现在可以指定行列和范围:
#line (startLine, startChar) - (endLine, endChar) charOffset "fileName"
// 比如 #line (1, 1) - (2, 2) 3 "test.cs"
以前在匹配嵌套属性的时候需要这么写:
if (a is { X: { Y: { Z: 4 } } }) { ... }
现在只需要简单的:
if (a is { X.Y.Z: 4 }) { ... }
实现接近零开销的字符串插值。
包括强类型的代码构建器,以及增量编译的支持等
C# 11 开始支持属性(attribute)为泛型类,即允许声明基类为System.Attribute
的泛型类:
public class GenericAttribute<T> : Attribute { }
C# 11 开始允许接口中定义静态方法(包括运算符重载方法),实现该接口的类必须包含该静态方法[25]:
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
对泛型及其对象进行数学操作的支持。基于静态接口方法特性,自 .NET 8.0 起,在System
命名空间中提供数学运算相关泛型接口,以支持泛型的运算操作[27]:
public static TResult Sum<T, TResult>(IEnumerable<T> values)
where T : INumber<T>
where TResult : INumber<TResult>
{
TResult result = TResult.Zero;
foreach (var value in values)
{
result += TResult.Create(value);
}
return result;
}
允许内插字符串中{
与}
内的文本跨多个行
原始字符串文本以 """
开始并以 """
结束,允许多行字符串,若为多行字符串则以单独的一行 """
结束,且字符串的缩进以末尾的 """
的起始位置为基准。原始字符串文本不进行任何转义操作,但允许字符串内插(开头的 $ 数量代表内插所需要的花括号数)[28]:
var x = 1;
var y = 2;
var code1 = """int i = 0;""";
var code2 = $"""int x = {x};""";
var code3 = $$"""
#include <stdio.h>
int main(void) {
const char *s = "{y} = {{y}}"; // {y} = 2
return 0;
}
""";
Console.WriteLine($"code1:\n{code1}\n");
Console.WriteLine($"code2:\n{code2}\n");
Console.WriteLine($"code3:\n{code3}\n");
可以对字符串字面量指定 u8
后缀来指定 UTF-8 字符编码的字符串字面量,其类型为ReadOnlySpan<byte>
[29]:
使用[
和]
可以定义列表模式,用于模式匹配:
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers is [1, 2, 3]); // True
Console.WriteLine(numbers is [1, 2, 4]); // False
Console.WriteLine(numbers is [1, 2, 3, 4]); // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]); // True
C# 11 起 nint
和 nuint
类型的别名分别为 IntPtr
和 UIntPtr
(C# 9 中它们仅被认为是“相似”的[30])。
优化了方法组向委托转换的性能。例如下述代码中,在 C# 11 前,Sum
比 SumMethodGroup
性能更高[31]:
static readonly List<int> Numbers = Enumberable.Range(0, 100).ToList();
public int Sum()
{
return Numbers.Where(x => Filter(x)).Sum(); // <- faster
}
public int SumMethodGroup()
{
return Numbers.Where(Filter).Sum(); // <- slower
}
static bool Filter(int number)
{
return number > 50;
}
params
修饰符不再仅限于数组类型。现在可以将 params
用于任何已识别的集合类型,包括 System.Span<T>
、System.ReadOnlySpan<T>
以及实现 System.Collections.Generic.IEnumerable<T>
并具有 Add
方法的类型。除了具体类型外,接口 System.Collections.Generic.IEnumerable<T>
、System.Collections.Generic.IReadOnlyCollection<T>
、System.Collections.Generic.IReadOnlyList<T>
、System.Collections.Generic.ICollection<T>
和 System.Collections.Generic.IList<T>
也可以使用。[33]
当使用接口类型时,编译器会合成提供的参数的存储。详情请参考Params collections的功能规范。
.NET 9 运行时引入了一种新的线程同步类型 System.Threading.Lock
,该类型通过其 API 提供了更好的线程同步。Lock.EnterScope()
方法进入一个排他作用域,返回的 ref struct
支持 Dispose()
模式以退出排他作用域。C# 的 lock
语句识别 Lock
对象,并使用更新的 API,而不是传统的 System.Threading.Monitor
API。如果将 Lock
对象转换为其他类型,编译器会生成基于 Monitor
的代码。[34]详情请参考该对象的功能规范。
可以使用 \e
作为 ESCAPE
字符 ( Unicode U+001B
) 的字面值转义串行。 在该版本以前,ESCAPE
使用的是 \u001b
或 \x1b
。[35]
不建议使用 \x1b
,因为如果 1b
后面的下一个字符是有效的十六进制数字,则那些字符会成为转义串行的一部分。[35]
该特性对涉及方法组的重载解析进行了小幅优化。方法组是指具有相同名称的所有重载方法。此前,编译器会构建方法组的完整候选方法集,并从中确定自然类型。新的行为是在每个作用域修剪候选方法集,移除不适用的方法(通常是具有错误泛型参数或不满足约束的泛型方法)。如果在给定作用域中找到的所有候选方法都不匹配,则方法组没有自然类型。[36]
以下是新行为的具体改进:
优化了方法组自然类型的确定:
var x = M;
)。在 C# 10 中,方法组获得了一种弱自然类型。这种类型是“弱类型”,仅在方法组未被目标类型化时才会发挥作用(即它在 System.Action a = MethodGroup;
中不起作用)。这种弱自然类型允许诸如 var x = MethodGroup;
的场景。[39]
方法组在所有候选方法具有共同签名时具有自然类型。如果方法组可能包含扩展方法,则候选方法包括包含类型和所有扩展方法作用域。
在实践中,这意味着我们将:
原则是按作用域逐步进行,并尽早修剪我们知道无法成功的候选方法(与重载解析中使用的原则相同)。
对于每个作用域,我们构建所有候选方法的集合:
现在可以在对象初始化表达式中使用隐式“从末尾”索引运算符 ^
[41]。
例如,可以在对象初始化器中初始化数组:
var countdown = new TimerRemaining()
{
buffer =
{
[^1] = 0,
[^2] = 1,
[^3] = 2,
[^4] = 3,
[^5] = 4,
[^6] = 5,
[^7] = 6,
[^8] = 7,
[^9] = 8,
[^10] = 9
}
};
上述示例创建了一个从 9 到 0 递减的数组。
在 C# 13 之前,^
运算符不能在对象初始化器中使用,必须从前面索引元素。
在 C# 13 之前,迭代器方法(使用 yield return
的方法)和异步方法不能声明本地 ref
变量,也不能有 unsafe
上下文。在 C# 13 中,异步方法可以声明本地 ref
变量或 ref struct
类型的本地变量,但这些变量不能跨越 await
边界访问。同样,它们也不能跨越 yield return
边界访问。这一放宽的限制使编译器能够在更多地方允许可验证的安全使用 ref
本地变量和 ref struct
类型。你可以在这些方法中安全地使用 System.ReadOnlySpan<T>
等类型。如果违反了安全规则,编译器会发出警告。[42]
在 C# 13 之前,ref struct
类型不能实现接口。从 C# 13 开始,它们可以实现接口。为了确保 ref
安全规则,ref struct
类型不能转换为接口类型。这是一种装箱转换,可能违反 ref
安全。[43]
在 C# 13 之前,ref struct
类型不能作为泛型类型或方法的类型参数声明。现在,泛型类型声明可以添加反约束 allows ref struct
。这种反约束声明该类型参数提供的类型参数可以是 ref struct
类型。编译器在该类型参数的所有实例上强制执行 ref
安全规则。这使得 System.Span<T>
和 System.ReadOnlySpan<T>
等类型可以在适用的地方与泛型算法一起使用。[44]
在 C# 13 中,可以声明分部属性和分部索引器。分部属性和索引器通常遵循与分部方法相同的规则:创建一个声明声明和一个实现声明。两个声明的签名必须匹配。一个限制是不能为分部属性使用自动属性声明。未声明主体的属性被视为声明声明。[45]详情请参阅Partial members文章。
在 C# 13 中,编译器识别 OverloadResolutionPriorityAttribute
以优先选择一个重载而不是另一个。库作者可以使用此属性确保新的、更好的重载优先于现有重载。例如,你可能会添加一个性能更高的新重载。你不希望破坏使用你库的现有代码,但希望用户在重新编译时更新到新版本。你可以使用重载解析优先级来通知编译器应优先选择哪个重载。优先级最高的重载会被优先选择。此功能旨在帮助库作者在添加新重载时避免歧义。库作者应谨慎使用此属性以避免混淆。[46]
C#通常不被编译成为能够直接在计算机上执行的二进制本地代码。与Java类似,它被编译成为中间代码(Microsoft Intermediate Language),然后通过.NET Framework的虚拟机——被称为通用语言运行库——执行。
所有的.Net编程语言都被编译成这种被称为通用中间语言的中间代码。因此虽然最终的程序在表面上仍然与传统意义上的可执行文件都具有“.exe”的后缀名。如果计算机上没有安装.Net Framework,那么这些程序会弹出对话框,要求用户下载.net framework。
在程序执行时,.Net Framework将中间代码翻译成为二进制机器码,从而使它得到正确的运行。最终的二进制代码被存储在一个缓冲区(Buffer)中。所以一旦程序使用了相同的代码,那么将会调用缓冲区中的版本。这样如果一个.Net程序第二次被运行,那么这种翻译不需要进行第二次,速度明显加快。
微软公司已经向ECMA申请将C#作为一种标准。在2001年12月,ECMA发布了ECMA-334 C#语言规范。C#在2003年成为一个ISO标准(ISO/IEC 23270)。现在有一些独立的实现正在进行,包括:
下面是一个在命令行上输出Hello World的小程序,这种程序通常作为开始学习程序语言的第一个步骤:
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
微软正在引领开源参考 C# 编译器和工具集的开发。 第一个编译器 Roslyn编译成中间语言(IL),第二个编译器 RyuJIT,[47] 是一个 JIT(即时)编译器,它是动态的,进行动态优化并编译将 IL 转换为 CPU 前端的本机代码。[48] RyuJIT 是开源的,用 C++ 编写。[49] Roslyn 完全是用 托管代码 (C#)编写的,已经开放并且功能以 API 的形式出现。因此,它使开发人员能够创建重构和诊断工具。[2][50] 官方实现的两个分支是 .NET Framework(闭源,仅限 Windows)和 .NET Core(开源,跨平台);它们最终融合为一个开源实现:.NET 5.0。[51] 在 .NET Framework 4.6 中,新的 JIT 编译器取代了前者。[47][52]
其他 C# 编译器(其中一些包括公共语言基础结构和 .NET 类库的实现):
游戏引擎 Unity 使用C# 作为其主要脚本语言。由于Microsoft 捐赠了 24,000 美元, Godot 游戏引擎实现了一个可选的 C# 模块。
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.