C# 二十年语法变迁之 C# 2,C# 3 ,C# 4参考

C# 二十年语法变迁之 C# 2,C# 3 ,C# 4参考

https://benbowen.blog/post/two_decades_of_csharp_i/

自从 C# 于 2000 年推出以来,该语言的规模已经大大增加,我不确定任何人是否有可能在任何时候都对每一种语言特性都有深入的了解。因此,我想写一系列快速参考文章,总结自 C# 2.0 以来所有主要的新语言特性。我不会详细介绍它们中的任何一个,但我希望这个系列可以作为我自己(希望你也是!)的参考,我可以不时回过头来记住我使用的工具工具箱里有。😃

开始之前的一个小提示:我将跳过一些更基本的东西(例如 C# 2.0 引入了泛型,但它们的使用范围如此广泛,以至于它们不值得包括在内);而且我还可以将一些功能“粘合”在一起,以使其更简洁。本系列并不打算成为该语言的权威或历史记录。相反,它更像是可能派上用场的重要语言功能的“备忘单”。

C# 2.0

可空值类型

这些允许您将null指定为任何结构变量的潜在值(否则null将无效):

class MyClass {
	public int MyInt { get; }
	public int? MyNullableInt { get; } // This property can be null even though it's of type 'int'
	
	public MyClass(int? input) { // input can be null
		MyInt = input != null ? input.Value : 0; // .Value throws an exception when accessed if input is null
		MyNullableInt = input;
	}
}

// ..

static void Test() {
	var mc = new MyClass(null);
	if (mc.MyNullableInt == null) Console.WriteLine("Was null!");
}

• “可空值类型”
从技术上讲,任何可空值对象的类型都是Nullable,其中T是实际包含的类型(即int?与Nullable相同)。Nullable本身就是一个结构,因此理论上检查该类型的实例是否为null是没有意义的,但是编译器和Nullable上重写的Equals()实现的组合允许我们将实例视为类型好像它确实有意义。您还可以通过使用其HasValue属性来确定Nullable实例是否为“非空” 。

部分类型partial class

此功能允许将大型类型(类、接口或结构)的实现分散到多个文件中。

// File: ExampleClass.Alpha.cs

public partial class ExampleClass {
	public void DoAlphaOne() { ... }
	
	public void DoAlphaTwo() { ... }
}





// File: ExampleClass.Beta.cs

public partial class ExampleClass {
	public void DoBetaOne() { ... }
	
	public void DoBetaTwo() { ... }
}




// Elsewhere

static void Test() {
	var ec = new ExampleClass();
	
	ec.DoAlphaOne();
	ec.DoBetaTwo();
	// etc
}

• “部分类”

空值合并

此功能允许创建计算为链中第一个非空值的表达式:

var neverNullString = _stringField ?? stringParameter ?? "Default";

全屏查看代码• “Null Coalesceing”
此代码会将neverNullString设置为_stringField,除非_stringField为空;在这种情况下,它将把它设置为stringParameter,除非stringParameter也为空,在这种情况下,它将把它设置为文字值"Default"。

迭代器生成器(Yield)

此功能允许您通过在可枚举中“生成”元素来创建IEnumerable或IEnumerator。下面的例子演示了我们如何创建一个由三个或六个整数组成的序列:

public IEnumerable<int> GetOneTwoThree(bool includeNegative = false) {
	yield return 1;
	yield return 2;
	yield return 3;
	if (!includeNegative) yield break;
	yield return -1;
	yield return -2;
	yield return -3;
}

// ..

Console.WriteLine(String.Join(",", GetOneTwoThree())); // Prints "1,2,3" on console
Console.WriteLine(String.Join(",", GetOneTwoThree(true))); // Prints "1,2,3,-1,-2,-3" on console

全屏查看代码• “收益回报和中断”

C# 3.0

扩展方法

此功能允许在预先存在的类型上定义新方法。这对于向您无法控制的类型添加功能很有用。以下示例显示
如何将ToString()的重载添加到double类型:

public static class DoubleExtensions {
	public static string ToString(this double @this, int numDecimalPlaces) {
		return @this.ToString("N" + numDecimalPlaces.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); 
	}
}

// ... Elsewhere ...

// Will write something like "3.5"; assuming YearsWorkedAtCompany is a double:
Console.WriteLine(user.YearsWorkedAtCompany.ToString(1)); 

• “ ToString 扩展”

集合初始化器

此功能允许实例化各种集合类型,同时向它们添加初始值:

var myArray = new[] { 1, 2, 3, 4, 5 }; // myArray is an int[] of length 5
var myList = new List<int> { 1, 2, 3, 4, 5 }; // myList is a List<int> with 5 elements
var myDict = new Dictionary<int, int> { { 1, 100 }, { 2, 200 } }; // myDict is a Dictionary with 2 key-value pairs

• “集合初始化器”

对象初始化器

此功能允许在其实例化点设置对象的内联属性:

class Person {
	public string Name { get; set; }
	public int Age { get; set; }
}

static void Test() {
	var person = new Person {
		Name = "Ben",
		Age = 30
	};
}

• “对象初始化器”

Partial Methods

与部分类一样,这允许您在不同的文件中编写方法的两个或多个部分。不保证执行顺序。该方法必须具有void返回类型,并且必须是私有的。

// File: ExampleClass.Alpha.cs

public partial class ExampleClass {
	public void Print() => DoThing();

	partial void DoThing() {
		Console.WriteLine("AAA");
	}
}





// File: ExampleClass.Beta.cs

public partial class ExampleClass {
	partial void DoThing() {
		Console.WriteLine("BBB");
	}
}




// Elsewhere

static void Test() {
	var ec = new ExampleClass();
	
	ec.Print(); // Prints AAA and BBB to console (order unspecified).
}

• “部分方法”
自动生成的代码可以使用空的部分方法声明,以允许用户在需要时手动插入自定义逻辑:

// File: ExampleClass.AutoGenerated.cs

public partial class ExampleClass {
	public void SomeAutoGeneratedMethod() {
		DoSomethingOnAutoGenMethodCall();
		
		// do other stuff
	}

	partial void DoSomethingOnAutoGenMethodCall(); // If user does not supply implementation in ExampleClass.User.cs this method will not even be compiled and calls to it will be removed
}





// File: ExampleClass.User.cs

public partial class ExampleClass {
	partial void DoSomethingOnAutoGenMethodCall() {
		Console.WriteLine("SomeAutoGeneratedMethod Invoked");
	}
}

• “自动生成代码的部分方法”

C# 4.0

动态/后期绑定类型

引入了动态类型以允许“后期绑定”类型解析。我偶尔使用动态的一件事是作为一种更简洁的反射形式:

class GenericClass<T1, T2> {
	public T1 SomeComplexMethod<T3>(T2 inputA, T3 inputB) {
		// ...
	}
}

class Program {
	static void Main() {
		var gc = new GenericClass<int, string>();
		var resultA = InvokeComplexMethodReflectively(gc, "Hi", 3f);
		var resultB = InvokeComplexMethodDynamically(gc, "Hi", 3f);

		Console.WriteLine(resultA);
		Console.WriteLine(resultB);
	}

	static object InvokeComplexMethodReflectively(object genericClassInstance, string inputA, float inputB) {
		var openMethodDefinition = genericClassInstance.GetType().GetMethod("SomeComplexMethod");
		var genericMethodDefinition = openMethodDefinition.MakeGenericMethod(typeof(float));
		return genericMethodDefinition.Invoke(genericClassInstance, new object[] { inputA, inputB });
	}

	static object InvokeComplexMethodDynamically(object genericClassInstance, string inputA, float inputB) {
		return ((dynamic) genericClassInstance).SomeComplexMethod(inputA, inputB);
	}
}

• “动态替代反射”
很多人根本就对使用动态有所保留,但是后期绑定无论如何都是在内部使用反射解决的,所以它真的可以被认为是反射的语法糖。此外,由于绑定信息的缓存,动态实际上通常可以胜过使用“纯”反射的相同方法。

C# 还添加了一个名为ExpandoObject的新类型,它类似于字典,但键是动态添加的成员:

static void Test() {
	dynamic user = new ExpandoObject();
	user.Name = "Ben";
	user.Age = 30;
	
	// Prints "User Name is Ben" and "User Age is 30"
	foreach (var kvp in (IDictionary<String, Object>) user) {
		Console.WriteLine($"User {kvp.Key} is {kvp.Value}");
	}
}

• “ExpandoObject”

可选参数

可选参数是具有指定默认值的方法的参数,因此不需要由调用者指定:

// 'nickname' is optional here
public static void MethodWithOptionalArgs(string name, int age, string nickname = null) {
	// ...
}

static void Test() {
	MethodWithOptionalArgs("Ben", 30); // No nickname specified, 'null' will be passed in
}

• “可选参数”
可选参数必须始终排在参数列表的最后。

命名参数

C# 4.0 中的命名参数允许在有多个参数时指定特定的可选参数:

public static void MethodWithOptionalArgs(string name, int age, string nickname = null, bool married = false, string address = null) {
	// ...
}

static void Test() {
	MethodWithOptionalArgs("Ben", 30, address: "Noveria"); // 'nickname' and 'married' are left unspecified
}

• “可选参数”
从 C# 7.2 开始,即使参数不是可选的,也可以命名。我偶尔在指定一个布尔参数时使用它,否则它很难理解:

static void Test() {
	CreateTables(true); // What is true?
	
	CreateTables(deletePreviousData: true); // Ahh... Much better.
}

• “可选参数”

泛型类型参数的协方差和逆变

在使用泛型类型参数创建接口或委托类型时,我们可以指定这些类型参数是协变的或逆变的:

interface ICovariant<out T> { }
interface IContravariant<in T> { }

static void Test() {
	ICovariant<object> covariantObj;
	ICovariant<int> covariantInt = GetCovariant<int>();
	
	covariantObj = covariantInt; // "out T" in ICovariant allows this
	
	
	
	IContravariant<object> contravariantObj = GetContravariant<object>();
	IContravariant<int> contravariantInt;
	
	contravariantInt = contravariantObj; // "in T" in IContravariant allows this
}

• “接口中的协变和逆变通用参数”

delegate T Covariant<out T>();
delegate void Contravariant<in T>(T value);

static void Test() {
	Covariant<object> covariantObj;
	Covariant<int> covariantInt = () => 3;
	
	covariantObj = covariantInt;
	


	Contravariant<object> contravariantObj = myObj => Console.WriteLine(myObj);
	Contravariant<int> contravariantInt;
	
	contravariantInt = contravariantObj;
}

• “委托中的协变和逆变通用参数”

当指定通常作为接口输出 的对象类型时,协方差很有用;因为我们通常不介意实际的输出类型是否是指定类型的子类型。在指定通常作为接口输入

的对象类型时,逆变很有用。因为我们通常不介意预期的输入类型是否是给定类型的父类型。 与其记住“contra”和“co”方差之间的区别,我发现记住in用于输入而out用于输出(通常,无论如何)是有用的。

posted @ 2022-09-25 19:42  溪源More  阅读(139)  评论(0编辑  收藏  举报