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#设计目标:
- C#旨在设计成为一种“简单、现代、通用”,以及面向对象的程序设计语言
- 此种语言的实现,应提供对于以下软件工程要素的支持:强类型检查、数组维度检查、未初始化的变量引用检测、自动垃圾收集(Garbage Collection,指一种存储器自动释放技术)。软件必须做到强大、持久,并具有较强的编程生产力。
- 此种语言为在分布式环境中的开发提供适用的组件开发应用。
- 为使程序员容易迁移到这种语言,源代码的可移植性十分重要,尤其是对于那些已熟悉C和C++的程序员而言。
- 对国际化的支持非常重要。
- 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] |
语言特性
- 指针(Pointer)只能用于不安全模式之中。大多数对象访问透过安全的引用实现,以避免无效的调用,并且有许多算法用于验证溢出,指针只能用于调用值类型,以及受垃圾收集控制的托管对象。
- 对象不能被显式释放,代替为当不存在被引用时透过垃圾回收器回收。
- 只允许单一继承(single inheritance),但是一个类可以实现多个接口(interfaces)。
- C#比C++更加类型安全。默认的安全转换是隐含转换,例如由短整型转换为长整型和从派生类转换为基本类。而接口布尔型同整型,及枚举型同整型不允许隐含转换,非空指针(透过引用相似对象)同用户定义类型的隐含转换字段被显式的确定,不同于C++的复制构造函数。
- 数组声明语法不同("int[] a = new int[5]"而不是"int a[5]")。
- 枚举位于其所在的命名空间中。
- C#中没有模版(Template),但是在C# 2.0中引入了泛型(Generic programming),并且支持一些C++模版不支持的特性。比如泛型参数中的类型约束。另一方面,表达式不能像C++模版中被用于类型参数。
- 属性支持,使用类似访问成员的方式调用。
- 完整的反射支持。
C# 2.0的特性
针对于.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;
}
}
注意事项:
- foreach循环时考虑线程安全性,不要试图对被遍历的集合进行remove和add等操作
- IEnumerable接口是LINQ特性的核心接口。只有实现了IEnumerable接口的集合,才能执行相关的LINQ操作,比如select,where等
匿名方法类似于函数式编程中的闭包。[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的特性
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表达式为强类型委托或强类型表达式树。
注意事项:
- 如果只有一个参数,可以省略括号(),例如 item=>{Console.WriteLine("只有一个参数{0}的Lambda表达式",item); };
- 如果只有一个返回值的语句,可以省略花括号{}、return关键字、分号,例如 item => {return item % 2 == 0;};改写成:item =>item %2 == 0;
- Lambda表达式可以分配给Func,Action或Predicate委托。
编译器将自动生成私有变量和适当的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 methods)必须定义在分部类(partial classes)中
- 定义分部方法需要用partial做修饰符
- 分部方法不一定总是有执行内容的,也就是说定义的方法可以一句操作语句都没有
- 分部方法返回值必须是void
- 分部方法可以是静态(static)方法
- 分部方法可以包含参数,参数可以包含以下修饰词:this,ref,params
- 分部方法必须是私有(private)方法
例子:
partial class C
{
static partial void M(int i); // defining declaration
}
partial class C
{
static partial void M(int i)
{
dosomething();
}
}
C# 4.0的特性
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();
}
C# 5.0的特性
- C# Evolution Matrix
- Async Feature (补充: async和await是一对语法糖,允许开发人员非常轻松的调用基于TASK的异步编程)async-await关键字并不会真的创建一个线程池任务,完成这个动作依赖于被调用方法中的函数。这一点在许多C#的中文教程中被忽略,导致许多学习的新手误以为await关键字会直接创建一个新的线程池任务。
- Caller Information
C# 6.0的特性
- 只读 Auto 属性
- Auto 属性初始设置式
- 使用静态
- Null - 条件运算符
- 字符串插值
- 例外状况筛选条件
- nameof 表达式
- Catch 和 Finally 区块中的 Await
- 索引初始设置式
- 集合初始设置式的扩展方法
- 改进的重载解析
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();//只读属性
}
C# 7.0的特性
能够直接宣告一个变量在它要传入的地方,当成一个 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;
}
}
C# 7.1的特性
- async``Main方法
- default常值表达式
- 推断的 tuple 项目名称
C# 7.2的特性
- 具备实值类型的参考语义
- 无后置具名引数
- 数值常值中的前置下划线
- private protected 访问修饰符
C# 8.0的特性
- 可空引用类型
- await yield return可异步返回的迭代器
- Index 索引类型和Range区间类型
- 允许在声明接口时为接口成员提供默认实现
- 递归的模式匹配
- 表达式形式的Switch关键字
- 在编译器可做类型推断的情况下,允许进一步省略类型声明
C# 9的特性
记录类型, 是一种引用类型, 默认是不可变的。 记录类型的相等判断可以通过引用或者结构进行判断的。
- 优点:记录类型是轻量级的不可变类型,可以减少大量的代码, 可以按照结构和引用进行比较;
- 缺点:需要实例化大量的对象;
// 默认不可变的记录类型
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) 定义初始化代码, 当初始化/加载时执行, 可以类比类的静态构造函数, 但是是组件级别的。
- 必须是静态的、无参数的、无返回值的方法;
- 不能是范型方法,也不能包含在范型类中;
- 不能是私有函数,必须是公开 (public) 或者内部 (internal) 的函数;
static 修饰符添加到 lambda 表达式或匿名方法 。这将无法捕获局部变量或实例状态,从而防止意外捕获其他变量。
移除了分部方法的下述限制:
- 必须具有 void 返回类型。
- 不能具有 out 参数。
- 不能具有任何可访问性(隐式 private )。
如果创建对象的类型已知时,可以在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
C# 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的特性[24]
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;
}
C# 13的特性[32]
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 类库的实现):
- 微软的Rotor项目(Rotor Project,目前称为Shared Source Common Language Infrastructure),提供了通用语言运行库(Common Language Runtime)的实现与C# 编译器。但是Shared Source Common Language Infrastructure在2006年的2.0版后就停止了。
- 由Microsoft赞助的Mono 项目提供了C# 编译器,它提供了一个开源 C# 编译器、一个完整的 CLI 开源实现,同时也接近百分之百地实现了.NET Framework类库。而Mono后来衍伸出由微软认可的第三方包Xamarin。
- Dot GNU 项目(现已停产)也提供了另一个自由版本的C# 编译器,也提供了.NET Framework类库的实现。
游戏引擎 Unity 使用C# 作为其主要脚本语言。由于Microsoft 捐赠了 24,000 美元, Godot 游戏引擎实现了一个可选的 C# 模块。
参考文献
外部链接
Wikiwand in your browser!
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.