C# 2.0
泛型(Generics)
泛型是CLR 2.0中引入的最重要的新特性,使得可以在類、方法中對使用的類型進行參數化。
例如,這里定義了一個泛型類:
class MyCollection<T> { T variable1; private void Add(T param){ } }
使用的時候:MyCollection<string> list2 = new MyCollection<string>(); MyCollection<Object> list3 = new MyCollection<Object>();
泛型的好處
- 編譯時就可以保證類型安全
- 不用做類型裝換,獲得一定的性能提升
泛型方法、泛型委托、泛型接口
除了泛型類之外,還有泛型方法、泛型委托、泛型接口:
//泛型委托
public static delegate T1 MyDelegate<T1, T2>(T2 item);
MyDelegate<Int32, String> MyFunc = new MyDelegate<Int32, String>(SomeMethd);
//泛型接口
public class MyClass<T1, T2, T3> : MyInteface<T1, T2, T3>
{
public T1 Method1(T2 param1, T3 param2)
{
throw new NotImplementedException();
}
}
interface MyInteface<T1, T2, T3> {
T1 Method1(T2 param1, T3 param2);
}
//泛型方法
static void Swap<T>(ref T t1, ref T t2) {
T temp = t1; t1 = t2; t2 = temp;
}
String str1 = "a"; String str2 = "b";
Swap<String>(ref str1, ref str2);
泛型約束(constraints)
可以給泛型的類型參數上加約束,可以要求這些類型參數滿足一定的條件
約束 |
說明 |
where T: struct | 類型參數需是值類型 |
where T : class | 類型參數需是引用類型 |
where T : new() | 類型參數要有一個public的無參構造函數 |
where T : <base class name> | 類型參數要派生自某個基類 |
where T : <interface name> | 類型參數要實現了某個接口 |
where T : U | 這里T和U都是類型參數,T必須是或者派生自U |
這些約束,可以同時一起使用:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new() { // ... }
default 關鍵字
這個關鍵可以使用在類型參數上:
default(T);
對於值類型,返回0,引用類型,返回null,對於結構類型,會返回一個成員值全部為0的結構實例。
迭代器(iterator)
可以在不實現IEnumerable就能使用foreach語句,在編譯器碰到yield return時,它會自動生成IEnumerable 接口的方法。在實現迭代器的方法或屬性中,返回類型必須是IEnumerable, IEnumerator, IEnumerable<T>,或 IEnumerator<T>。迭代器使得遍歷一些零碎數據的時候很方便,不用去實現Current, MoveNext 這些方法。
public System.Collections.IEnumerator GetEnumerator() {
yield return -1; for (int i = 1; i < max; i++) { yield return i;
}
}
可空類型(Nullable Type)
可空類型System.Nullable<T>,可空類型僅針對於值類型,不能針對引用類型去創建。System.Nullable<T>簡寫為T ?。
int? num = null; if (num.HasValue == true) { System.Console.WriteLine("num = " + num.Value); } else { System.Console.WriteLine("num = Null"); }
如果HasValue為false,那么在使用value值的時候會拋出異常。把一個Nullable的變量x賦值給一個非Nullable的變量y可以這么寫:
int y = x ?? -1;
匿名方法(Anonymous Method)
在C#2.0之前,給只能用一個已經申明好的方法去創建一個委托。有了匿名方法后,可以在創建委托的時候直接傳一個代碼塊過去。
delegate void Del(int x); Del d = delegate(int k) { /* ... */ }; System.Threading.Thread t1 = new System.Threading.Thread (delegate() { System.Console.Write("Hello, "); } ); 委托語法的簡化// C# 1.0的寫法 ThreadStart ts1 = new ThreadStart(Method1); // C# 2.0可以這么寫 ThreadStart ts2 = Method1;
委托的協變和逆變(covariance and contravariance)
有下面的兩個類:
class Parent { } class Child: Parent { }
然后看下面的兩個委托:
public delegate Parent DelgParent();
public delegate Child DelgChild();
public static Parent Method1() { return null; }
public static Child Method2() { return null; }
static void Main() { DelgParent del1= Method1; DelgChild del2= Method2; del1 = del2; }
注意上面的,DelgParent 和DelgChild 是完全不同的類型,他們之間本身沒有任何的繼承關系,所以理論上來說他們是不能相互賦值的。但是因為協變的關系,使得我們可以把DelgChild類型的委托賦值給DelgParent 類型的委托。協變針對委托的返回值,逆變針對參數,原理是一樣的。
部分類(partial)
在申明一個類、結構或者接口的時候,用partial關鍵字,可以讓源代碼分布在不同的文件中。我覺得這個東西完全是為了照顧Asp.net代碼分離而引入的功能,真沒什么太大的實際用處。微軟說在一些大工程中可以把類分開在不同的文件中讓不同的人去實現,方便團隊協作,這個我覺得純屬胡扯。
部分類僅是編譯器提供的功能,在編譯的時候會把partial關鍵字定義的類和在一起去編譯,和CRL沒什么關系。
靜態類(static class)
靜態類就一個只能有靜態成員的類,用static關鍵字對類進行標示,靜態類不能被實例化。靜態類理論上相當於一個只有靜態成員並且構造函數為私有的普通類,靜態類相對來說的好處就是,編譯器能夠保證靜態類不會添加任何非靜態成員。
global::
這個代表了全局命名空間(最上層的命名空間),也就是任何一個程序的默認命名空間。
class TestApp { public class System { } const int Console = 7; static void Main() { //用這個訪問就會出錯,System和Console都被占用了 //Console.WriteLine(number); global::System.Console.WriteLine(number); } }
extern alias
用來消除不同程序集中類名重復的沖突,這樣可以引用同一個程序集的不同版本,也就是說在編譯的時候,提供了一個將有沖突的程序集進行區分的手段。
在編譯的時候,使用命令行參數來指明alias,例如:
/r:aliasName=assembly1.dll
在Visual Studio里面,在被引用的程序集的屬性里面可以指定Alias的值,默認是global。
然后在代碼里面就可以使用了:
extern alias aliasName; //這行需要在using這些語句的前面 using System; using System.Collections.Generic; using System.Text; using aliasName.XXX;
屬性Accessor訪問控制
public virtual int TestProperty { protected set { } get { return 0; } }
友元程序集(Friend Assembly)
可以讓其它程序集訪問自己的internal成員(private的還是不行),使用Attributes來實現,例如:
[assembly:InternalsVisibleTo("cs_friend_assemblies_2")]
注意這個作用范圍是整個程序集。
fixed關鍵字
可以使用fixed關鍵字來創建固定長度的數組,但是數組只能是bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float, double中的一種。
這主要是為了更好的處理一些非托管的代碼。比如下面的這個結構體:
public struct MyArray { public fixed char pathName[128]; }
如果不用fixed的話,無法預先占住128個char的空間,使用fixed后可以很好的和非托管代碼進行交互。
volatile關鍵字
用來表示相關的字可能被多個線程同時訪問,編譯器不會對相應的值做針對單線程下的優化,保證相關的值在任何時候訪問都是最新的。
#pragma warning
用來取消或者添加編譯時的警告信息。每個警告信息都會有個編號,如果warning CS01016之類的,使用的時候取CS后面的那個數字,例如:
#pragma warning disable 414, 3021
這樣CS414和CS3021的警告信息就都不會顯示了。
C# 3.0
類型推斷
申明變量的時候,可以不用直指定類型:
-
var i = 5;
-
var s = "Hello";
-
//兩種寫法是一樣的
-
int i = 5;
-
string s = "Hello";
類型推斷也支持數組:
-
var b = new[] { 1, 1.5, 2, 2.5 }; // double[]
-
var c = new[] { "hello", null, "world” }; // string[]
擴展方法
擴展方法必須被定義在靜態類中,並且必須是非泛型、非嵌套的靜態類。例如:
-
public static class JeffClass
-
{
-
public static int StrToInt32(this string s)
-
{
-
return Int32.Parse(s);
-
}
-
-
public static T[] SomeMethd<T>(this T[] source, int pram1, int pram2)
-
{
-
/**/
-
}
-
}
上面一個是給string類型的對象添加了一個方法,另一個是給所有類型的數組添加了一個方法,方法有兩個整型參數。
擴展方法只在當前的命名空間類有效,如果所在命名空間被其它命名空間import引用了,那么在其它命名空間中也有效。擴展方法的優先級低於其它的常規方法,也就是說如果擴展方法與其它的方法相同,那么擴展方法不會被調用。
Lamda表達式
可以看成是對匿名方法的一個語法上的簡化,但是λ表達式同時可以裝換為表達式樹類型。
對象和集合的初始化
-
var contacts = new List<Contact> {
-
new Contact {
-
Name = "Chris",
-
PhoneNumbers = { "123455", "6688" }
-
},
-
new Contact {
-
Name = "Jeffrey",
-
PhoneNumbers = { "112233" }
-
}
-
};
匿名類型
-
var p1 = new { Name = "Lawnmower", Price = 495.00 };
-
var p2 = new { Name = "Shovel", Price = 26.95 };
-
p1 = p2;
自動屬性
會自動生成一個后台的私有變量
-
public Class Point
-
{
-
public int X { get; set; }
-
public int Y { get; set; }
-
}
查詢表達式
這個其實就是擴展方法的運用,編譯器提供了相關的語法便利,下面兩端代碼是等價的:
-
from g in
-
from c in customers
-
group c by c.Country
-
select new { Country = g.Key, CustCount = g.Count() }
-
-
customers.
-
GroupBy(c => c.Country).
-
Select(g => new { Country = g.Key, CustCount = g.Count() })
表達式樹
-
Func< int,int> f = x => x + 1;
-
Expression<Func< int,int>> e = x => x + 1;
C# 4.0
協變和逆變
這個在C#2.0中就已經支持委托的協變和逆變了,C#4.0開始支持針對泛型接口的協變和逆變:
-
IList< string> strings = new List<string>();
-
-
IList< object> objects = strings;
協變和逆變僅針對引用類型。
動態綁定
看例子:
-
class BaseClass
-
{
-
public void print()
-
{
-
Console.WriteLine();
-
}
-
}
-
Object o = new BaseClass();
-
dynamic a = o;
-
//這里可以調用print方法,在運行時a會知道自己是個什么類型。 這里的缺點在於編譯的時候無法檢查方法的合法性,寫錯的話就會出運行時錯誤。
-
a.print();
可選參數,命名參數
private void CreateNewStudent(string name, int studentid = 0, int year = 1)
這樣,最后一個參數不給的話默認值就是1,提供這個特性可以免去寫一些重載方法的麻煩。
調用方法的時候,可以指定參數的名字來給值,不用按照方法參數的順序來制定參數值:
CreateNewStudent(year:2, name:"Hima", studentid: 4); //沒有按照方法定義的參數順序
C# 5.0
1. 異步編程
在.Net 4.5中,通過async和await兩個關鍵字,引入了一種新的基於任務的異步編程模型(TAP)。在這種方式下,可以通過類似同步方式編寫異步代碼,極大簡化了異步編程模型。如下式一個簡單的實例:
static async void DownloadStringAsync2(Uri uri)
{
var webClient = new WebClient();
var result = await webClient.DownloadStringTaskAsync(uri);
Console.WriteLine(result);
}
而之前的方式是這樣的:
static void DownloadStringAsync(Uri uri)
{
var webClient = new WebClient();
webClient.DownloadStringCompleted += (s, e) =>
{
Console.WriteLine(e.Result);
};
webClient.DownloadStringAsync(uri);
}
也許前面這個例子不足以體現async和await帶來的優越性,下面這個例子就明顯多了:
public void CopyToAsyncTheHardWay(Stream source, Stream destination)
{
byte[] buffer = new byte[0x1000];
Action<IAsyncResult> readWriteLoop = null;
readWriteLoop = iar =>
{
for (bool isRead = (iar == null); ; isRead = !isRead)
{
switch (isRead)
{
case true:
iar = source.BeginRead(buffer, 0, buffer.Length,
readResult =>
{
if (readResult.CompletedSynchronously) return;
readWriteLoop(readResult);
}, null);
if (!iar.CompletedSynchronously) return;
break;
case false:
int numRead = source.EndRead(iar);
if (numRead == 0)
{
return;
}
iar = destination.BeginWrite(buffer, 0, numRead,
writeResult =>
{
if (writeResult.CompletedSynchronously) return;
destination.EndWrite(writeResult);
readWriteLoop(null);
}, null);
if (!iar.CompletedSynchronously) return;
destination.EndWrite(iar);
break;
}
}
};
readWriteLoop(null);
}
public async Task CopyToAsync(Stream source, Stream destination)
{
byte[] buffer = new byte[0x1000];
int numRead;
while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
await destination.WriteAsync(buffer, 0, numRead);
}
}
關於基於任務的異步編程模型需要介紹的地方還比較多,不是一兩句能說完的,有空的話后面再專門寫篇文章來詳細介紹下。另外也可參看微軟的官方網站:Visual Studio Asynchronous Programming,其官方文檔Task-Based Asynchronous Pattern Overview介紹的非常詳細, VisualStudio中自帶的CSharp Language Specification中也有一些說明。
2. 調用方信息
很多時候,我們需要在運行過程中記錄一些調測的日志信息,如下所示:
public void DoProcessing()
{
TraceMessage("Something happened.");
}
為了調測方便,除了事件信息外,我們往往還需要知道發生該事件的代碼位置以及調用棧信息。在C++中,我們可以通過定義一個宏,然后再宏中通過__FILE__和__LINE__來獲取當前代碼的位置,但C#並不支持宏,往往只能通過StackTrace來實現這一功能,但StackTrace卻有不是很靠譜,常常獲取不了我們所要的結果。
針對這個問題,在.Net 4.5中引入了三個Attribute:CallerMemberName、CallerFilePath和CallerLineNumber。在編譯器的配合下,分別可以獲取到調用函數(准確講應該是成員)名稱,調用文件及調用行號。上面的TraceMessage函數可以實現如下:
public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}
另外,在構造函數,析構函數、屬性等特殊的地方調用CallerMemberName屬性所標記的函數時,獲取的值有所不同,其取值如下表所示:
調用的地方 |
CallerMemberName獲取的結果 |
方法、屬性或事件 |
方法,屬性或事件的名稱 |
構造函數 |
字符串 ".ctor" |
靜態構造函數 |
字符串 ".cctor" |
析構函數 |
該字符串 "Finalize" |
用戶定義的運算符或轉換 |
生成的名稱成員,例如, "op_Addition"。 |
特性構造函數 |
特性所應用的成員的名稱 |
例如,對於在屬性中調用CallerMemberName所標記的函數即可獲取屬性名稱,通過這種方式可以簡化 INotifyPropertyChanged 接口的實現。
-
C
-
1、自動屬性的增強
1.1、自動屬性初始化 (Initializers for auto-properties)
C#4.0下的果斷實現不了的。
C#6.0中自動屬性的初始化方式
只要接觸過C#的肯定都會喜歡這種方式。真是簡潔方便呀。
1.2、只讀屬性初始化Getter-only auto-properties
先來看一下我們之前使用的方式吧
public class Customer { public string Name { get; } public Customer(string firstName,string lastName) { Name = firstName +" "+ lastName; } }
再來看一下C#6.0中
public class Customer { public string FirstName { get; }="aehyok"; public string LastName { get; }="Kris"; }
和第一條自動屬性初始化使用方式一致。
2、Expression bodied function members
2.1 用Lambda作為函數體Expression bodies on method-like members
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
再來舉一個簡單的例子:一個沒有返回值的函數
public void Print() => Console.WriteLine(FirstName + " " + LastName);
2.2、Lambda表達式用作屬性Expression bodies on property-like function members
public override string ToString() { return FirstName + " " + LastName; }
現在C#6中
public class User { public string FirstName { get; set; } public string LastName { get; set; } public override string ToString() => string.Format("{0}——{1}", FirstName, LastName); public string FullName => FirstName + " " + LastName; }
3、引用靜態類Using Static
在Using中可以指定一個靜態類,然后可以在隨后的代碼中直接使用靜態的成員
4、空值判斷Null-conditional operators
直接來看代碼和運行結果
通過結果可以發現返回的都為null,再也不像以前那樣繁瑣的判斷null勒。
5、字符串嵌入值
在字符串中嵌入值
之前一直使用的方式是
現在我們可以簡單的通過如下的方式進行拼接
6、nameof表達式nameof expressions
在方法參數檢查時,你可能經常看到這樣的代碼(之前用的少,這次也算學到了)
public static void AddCustomer(Customer customer) { if (customer == null) { throw new ArgumentNullException("customer"); } }
里面有那個customer是我們手寫的字符串,在給customer改名時,很容易把下面的那個字符串忘掉,C#6.0 nameof幫我們解決了這個問題,看看新寫法
public static void AddCustomer(Customer customer) { if (customer == null) { throw new ArgumentNullException(nameof(customer)); } }
7、帶索引的對象初始化器Index initializers
直接通過索引進行對象的初始化,原來真的可以實現
通過這種方式可以發現字典中只有三個元素,所以也就只有這三個索引可以訪問額,其他類型的對象和集合也是可以通過這種方式進行初始化的,在此就不進行一一列舉了。
8、異常過濾器 (Exception filters)
先來看一個移植過來的方法
try { var numbers = new Dictionary<int, string> {[7] = "seven",[9] = "nine",[13] = "thirteen" }; } catch (ArgumentNullException e) { if (e.ParamName == "customer") { Console.WriteLine("customer can not be null"); } }
在微軟的文檔中還給出了另一種用法,這個異常會在日志記錄失敗時拋給上一層調用者
private static bool Log(Exception e) { ///處理一些日志 return false; } static void Main(string[] args) { try { /// } catch (Exception e){if (!Log(e)) { } } Console.ReadLine(); }
9、catch和finally 中的 await —— Await in catch and finally blocks
在C#5.0中,await關鍵字是不能出現在catch和finnaly塊中的。而在6.0中
try { res = await Resource.OpenAsync(…); // You could do this. … } catch (ResourceException e) { await Resource.LogAsync(res, e); // Now you can do this … } finally { if (res != null) await res.CloseAsync(); // … and this. }
10、無參數的結構體構造函數—— Parameterless constructors in structs
總的來說,這些新特性使 C# 7.0 更容易以函數式編程的思想來寫代碼,C# 6.0 在這條路上已經做了不少工作, C# 7.0 更近一步!
表達式 everywhere
C# 6.0 中,可以對成員方法和只讀屬性使用 Lambda 表達式,當時最郁悶的就是為什么不支持屬性的 set 訪問器。現在好了,不僅 set 方法器支持使用 Lambda 表達式,構造方法、析構方法以及索引都支持以 Lambda 表達式方式定義了。
-
class SomeModel
-
{
-
private string internalValue;
-
-
public string Value
-
{
-
get => internalValue;
-
set => internalValue = string.IsNullOrWhiteSpace(value) ? null : value;
-
}
-
}
out
變量
out
變量是之前就存在的語法,C# 7.0 只是允許它將申明和使用放在一起,避免多一行代碼。最直接的效果,就是可以將兩個語句用一個表達式完成。這里以一個簡化版的 Key
類為例,這個類早期被我們用於處理通過 HTTP Get/Post 傳入的 ID 值。
-
public class Key
-
{
-
public string Value { get; }
-
-
public Key(string key)
-
{
-
Value = key;
-
}
-
-
public int IntValue
-
{
-
get
-
{
-
// C# 6.0,需要提前定義 intValue,但不需要初始化
-
// 雖然 C# 6.0 可以為只讀屬性使用 Lambda 表達式
-
// 但這里無法用一個表達式表達出來
-
int intValue;
-
return int.TryParse(Value, out intValue) ? intValue : 0;
-
}
-
}
-
}
而在 C# 7 中就簡單了
-
// 注意 out var intValue,
-
// 對於可推導的類型甚至可以用 var 來申明變量
-
public int IntValue => int.TryParse(Value, out var intValue) ? intValue : 0;
元組和解構
用過 System.Tuple
的朋友一定對其 Item1
、Item2
這樣毫無語義的命名深感不爽。不過 C# 7.0 帶來了語義化的命名,同時,還減化了元組的創建,不再需要 Tuple.Create(...)
。另外,要使用新的元組特性和解構,需要引入 NuGet 包 System.ValueTuple
。
Install-Package System.ValueTuple
當然,元組常用於返回多個值的方法。也有些人喜歡用 out
參數來返回,但即使現在可以 out
變量,我仍然不贊成廣泛使用 out
參數。
下面這個示例方法用於返回一個默認的時間范圍(從今天開始算往前一共 7 天),用於數據檢索。
-
// 返回類型是一個包含兩個元素的元組
-
(DateTime Begin, DateTime End) GetDefaultDateRange()
-
{
-
var end = DateTime.Today.AddDays(1);
-
var begin = end.AddDays(-7);
-
-
// 這里使用一對圓括號就創建了一個元組
-
return (begin, end);
-
}
調用這個方法可以獲得元組,因為定義的時候返回值指定了每個數據成員的名稱,所以從元組獲取數據可以是語義化的,當然仍然可以使用 Item1
和 Item2
。
-
var range = GetDefaultDateRange();
-
var begin = range.Begin; // 也可以 begin = range.Item1
-
var end = range.End; // 也可以 end = range.Item2
上面這個例子還可以簡化,不用 range
這個中間變量,這就用到了解構
var (begin, end) = GetDefaultDateRange();
這里創建元組是以返回值來舉例的,其實它就是一個表達式,可以在任何地方創建元組。上面的例子邏輯很簡單,可以用表達式解決。下面的示例順便演示了非語義化的返回類型申明。
-
// 原來的 (DateTime Begin, DateTime End) 申明也是沒問題的
-
(DateTime, DateTime) GetDefaultDateRange()
-
=> (DateTime.Today.AddDays( 1).AddDays(-7), DateTime.Today.AddDays(1));
解構方法 Deconstrct
解構方法可以讓任何類(而不僅僅是元組)按定義的參數進行解構。而且神奇的是解構方法可以是成員方法,也可以定義成擴展方法。
-
public class Size
-
{
-
public int Width { get; }
-
public int Height { get; }
-
public int Tall { get; }
-
-
public Size(int width, int height, int tall)
-
{
-
this.Width = width;
-
this.Height = height;
-
this.Tall = tall;
-
}
-
-
// 定義成成員方法的解構
-
public void Deconstruct(out int width, out int height)
-
{
-
width = Width;
-
height = Height;
-
}
-
}
-
-
public static class SizeExt
-
{
-
// 定義成擴展方法的解構
-
public static void Deconstruct(this Size size, out int width, out int height, out int tall)
-
{
-
width = size.Width;
-
height = size.Height;
-
tall = size.Tall;
-
}
-
}
下面是使用解構的代碼
-
var size = new Size(1920, 1080, 10);
-
var (w, h) = size;
-
var (x, y, z) = size;
改造 Size
的構造方法
還記得前面提到的構造方法可以定義為 Lambda 表達式嗎?下面是使用元組和 Lambda 對 Size 構造方法的改造——我已經醉了!
-
public Size(int width, int height, int tall)
-
=> (Width, Height, Tall) = (width, height, tall);
模式匹配
模式匹配目前支持 is
和 switch
。說起來挺高大上的一個名字,換個接地氣一點的說法就是判斷類型順便定義個具體類型的引用,有興趣還可以加再點額外的判斷。
對於 is
來說,就是判斷的時候順便定義個變量再初始化一下,所以像原來這樣寫的代碼
-
// 假設邏輯能保證這里的 v 可能是 string 也 可能是 int
-
string ToString(object v) {
-
if (v is int) {
-
int n = (int) v;
-
return n.ToString("X4");
-
} else {
-
return (string) n;
-
}
-
}
可以簡化成——好吧,直接一步到位寫成表達式好了
-
string ToString(object v)
-
=> (v is int n) ? n.ToString("X4") : (string) v;
當然你可能說之前的那個也可以簡化成一個表達式——好吧,不深究這個問題好嗎?我只是演示
is
的模式匹配而已。
而 switch
中的模式匹配似乎要有用得多,還是以 ToString 為例吧
-
static string ToString(object v)
-
{
-
switch (v)
-
{
-
case int n when n > 0xffff:
-
// 判斷類型,匹配的情況下再對值進行一個判斷
-
return n.ToString("X8");
-
case int n:
-
// 判斷類型,這里 n 肯定 <= 0xffff
-
return n.ToString("X4");
-
case bool b:
-
return b ? "ON" : "OFF";
-
case null:
-
return null;
-
default:
-
return v.ToString();
-
}
-
}
注意一下上面第一個分支中 when
的用法就好了。
ref 局部變量和 ref 返回值
這已經是很接近 C/C++ 的一種用法了。雖然官方說法是這樣做可以解決一些安全性問題,但我個人目前還是沒遇到它的使用場景。如果設計足夠好,在目前又加入了元組新特性和解構的情況下,個人認為幾乎可以避免使用 out
和 ref
。
既然沒用到,我也不多說了,有用到的同學來討論一下!
數字字面量語法增強
這里有兩點增強,一點是引入了 0b
前綴的二進制數字面量語法,另一點是可以在數值字面量中任意使用 _
對數字進行分組。這個不用多數,舉兩個例就明白了
-
const int MARK_THREE = 0b11; // 0x03
-
const int LONG_MARK = 0b_1111_1111; // 0xff
-
const double PI = 3.14_1592_6536
局部函數
經常寫 JavaScript 的同學肯定會深有體會,局部函數是個好東西。當然它在 C# 中帶來的最大好處是將某些代碼組織在了一起。我之前在項目中大量使用了 Lambda 來代替局部函數,現在可以直接替換成局部函數了。Labmda 和局部函數雖然多數情況下能做同樣的事情,但是它們仍然有一些區別
-
對於 Lambda,編譯器要干的事情比較多。總之呢,就是編譯效率要低得多
-
Lambda 通過委托實現,調用過程比較復雜,局部函數可以直接調用。簡單地說就是局部函數執行效率更高
-
Lambda 必須先定義再使用,局部函數可以定義在使用之后。據說這在對遞歸算法的支持上會有區別
比較常用的地方是 Enumerator 函數和 async 函數中,因為它們實際都不是立即執行的。
我在項目中多是用來組織代碼。局部函數代替只被某一個公共 API 調用的私有函數來組織代碼雖然不失為一個簡化類結構的好方法,但是把公共 API 函數的函數體拉長。所以很多時候我也會使用內部類來代替某些私有函數來組織代碼。這里順便說一句,我不贊成使用 #region
組織代碼。
支持更多 async 返回類型
如果和 JavaScript 中 ES2017 的 async 相比,C# 中的 Task/Task<T>
就比較像 Promise
的角色。不用羡慕 JavaScript 的 async 支持 Promise like,現在 C# 的 async 也支持 Task like 了,只要實現了 GetAwaiter
方法就行。
官方提供了一個 ValueTask
作為示例,可以通過 NuGet 引入:
Install-Package System.Threading.Tasks.Extensions
這個 ValueTask
比較有用的一點就是兼容了數據類型和 Task:
-
string cache;
-
-
ValueTask<string> GetData()
-
{
-
return cache == null ? new ValueTask<string>(cache) : new ValueTask<string>(GetRemoteData());
-
-
// 局部函數
-
async Task<string> GetRemoteData()
-
{
-
await Task.Delay(100);
-
return "hello async";
-
}
-
}
C#7.0
4.ref
locals and returns (局部變量和引用返回)
6.More expression-bodied members(更多的函數成員的表達式體)
8.Generalized async return types (通用異步返回類型)
9.Numeric literal syntax improvements(數值文字語法改進)
1. out-variables(Out變量)
以前,我們使用out變量的時候,需要在外部先申明,然后才能傳入方法,類似如下:
string ddd = ""; //先申明變量 ccc.StringOut(out ddd); Console.WriteLine(ddd);
在C#7.0中我們可以不必申明,直接在參數傳遞的同時申明它,如下:
StringOut(out string ddd); //傳遞的同時申明 Console.WriteLine(ddd); Console.ReadLine();
2.Tuples(元組)
曾今在.NET4.0中,微軟對多個返回值給了我們一個解決方案叫元組,類似代碼如下:
static void Main(string[] args) { var data = GetFullName(); Console.WriteLine(data.Item1); Console.WriteLine(data.Item2); Console.WriteLine(data.Item3); Console.ReadLine(); } static Tuple<string, string, string> GetFullName() { return new Tuple<string, string, string>("a", "b", "c"); }
上面代碼展示了一個方法,返回含有3個字符串的元組,然而當我們獲取到值,使用的時候 心已經炸了,Item1,Item2,Item3是什么鬼,雖然達到了我們的要求,但是實在不優雅
那么,在C#7.0中,微軟提供了更優雅的方案:(注意:需要通過nuget引用System.ValueTuple)如下:
static void Main(string[] args) { var data=GetFullName(); Console.WriteLine(data.a); //可用命名獲取到值 Console.WriteLine(data.b); Console.WriteLine(data.c); Console.ReadLine(); } //方法定義為多個返回值,並命名 private static (string a,string b,string c) GetFullName() { return ("a","b","c"); }
解構元組,有的時候我們不想用var匿名來獲取,那么如何獲取abc呢?我們可以如下:
static void Main(string[] args) { //定義解構元組 (string a, string b, string c) = GetFullName(); Console.WriteLine(a); Console.WriteLine(b); Console.WriteLine(c); Console.ReadLine(); } private static (string a,string b,string c) GetFullName() { return ("a","b","c"); }
3. Pattern Matching(匹配模式)
在C#7.0中,引入了匹配模式的玩法,先舉個老栗子.一個object類型,我們想判斷他是否為int如果是int我們就加10,然后輸出,需要如下:
object a = 1; if (a is int) //is判斷 { int b = (int)a; //拆 int d = b+10; //加10 Console.WriteLine(d); //輸出 }
那么在C#7.0中,首先就是對is的一個小擴展,我們只需要這樣寫就行了,如下:
object a = 1; if (a is int c) //這里,判斷為int后就直接賦值給c { int d = c + 10; Console.WriteLine(d); }
這樣是不是很方便?特別是經常用反射的同志們..
那么問題來了,挖掘機技術哪家強?!(咳咳,呸 開玩笑)
其實是,如果有多種類型需要匹配,那怎么辦?多個if else?當然沒問題,不過,微軟爸爸也提供了switch的新玩法,我們來看看,如下:
我們定義一個Add的方法,以Object作為參數,返回動態類型
static dynamic Add(object a) { dynamic data; switch (a) { case int b: data=b++; break; case string c: data= c + "aaa"; break; default: data = null; break; } return data; }
下面運行,傳入int類型:
object a = 1; var data= Add(a); Console.WriteLine(data.GetType()); Console.WriteLine(data);
輸出如圖:
我們傳入String類型的參數,代碼和輸出如下:
object a = "bbbb"; var data= Add(a); Console.WriteLine(data.GetType()); Console.WriteLine(data);
通過如上代碼,我們就可以體會到switch的新玩法是多么的順暢和強大了.
匹配模式的Case When篩選
有的基友就要問了.既然我們可以在Switch里面匹配類型了,那我們能不能順便篩選一下值?答案當然是肯定的.
我們把上面的Switch代碼改一下,如下:
switch (a) { case int b when b < 0: data = b + 100; break; case int b: data=b++; break; case string c: data= c + "aaa"; break; default: data = null; break; }
在傳入-1試試,看結果如下:
4.ref locals and returns(局部變量和引用返回)
已經補上,請移步:http://www.cnblogs.com/GuZhenYin/p/6531814.html
5.Local Functions (局部函數)
嗯,這個就有點顛覆..大家都知道,局部變量是指:只在特定過程或函數中可以訪問的變量。
那這個局部函數,顧名思義:只在特定的函數中可以訪問的函數(媽蛋 好繞口)
使用方法如下:
public static void DoSomeing() { //調用Dosmeing2 int data = Dosmeing2(100, 200); Console.WriteLine(data); //定義局部函數,Dosmeing2. int Dosmeing2(int a, int b) { return a + b; } }
呃,解釋下來 大概就是在DoSomeing中定義了一個DoSomeing2的方法,..在前面調用了一下.(注:值得一提的是局部函數定義在方法的任何位置,都可以在方法內被調用,不用遵循逐行解析的方式)
6.More expression-bodied members(更多的函數成員的表達式體)
C#6.0中,提供了對於只有一條語句的方法體可以簡寫成表達式。
如下:
public void CreateCaCheContext() => new CaCheContext(); //等價於下面的代碼 public void CreateCaCheContext() { new CaCheContext(); }
但是,並不支持用於構造函數,析構函數,和屬性訪問器,那么C#7.0就支持了..代碼如下:
// 構造函數的表達式寫法 public CaCheContext(string label) => this.Label = label; // 析構函數的表達式寫法 ~CaCheContext() => Console.Error.WriteLine("Finalized!"); private string label; // Get/Set屬性訪問器的表達式寫法 public string Label { get => label; set => this.label = value ?? "Default label"; }
7.throw
Expressions (異常表達式)
在C#7.0以前,我們想判斷一個字符串是否為null,如果為null則拋除異常,我們需要這么寫:
public string IsNull() { string a = null; if (a == null) { throw new Exception("異常了!"); } return a; }
這樣,我們就很不方便,特別是在三元表達式 或者非空表達式中,都無法拋除這個異常,需要寫if語句.
那么我們在C#7.0中,可以這樣:
public string IsNull() { string a = null; return a ?? throw new Exception("異常了!"); }
8.Generalized async return types (通用異步返回類型)
嗯,這個,怎么說呢,其實我異步用的較少,所以對這個感覺理解不深刻,還是覺得然並卵,在某些特定的情況下應該是有用的.
我就直接翻譯官方的原文了,實例代碼也是官方的原文.
異步方法必須返回 void,Task 或 Task<T>,這次加入了新的ValueTask<T>,來防止異步運行的結果在等待時已可用的情境下,對 Task<T> 進行分配。對於許多示例中設計緩沖的異步場景,這可以大大減少分配的數量並顯著地提升性能。
官方的實例展示的主要是意思是:一個數據,在已經緩存的情況下,可以使用ValueTask來返回異步或者同步2種方案
public class CaCheContext { public ValueTask<int> CachedFunc() { return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(loadCache()); } private bool cache = false; private int cacheResult; private async Task<int> loadCache() { // simulate async work: await Task.Delay(5000); cache = true; cacheResult = 100; return cacheResult; } }
調用的代碼和結果如下:
//main方法可不能用async修飾,所以用了委托. static void Main(string[] args) { Action act = async () => { CaCheContext cc = new CaCheContext(); int data = await cc.CachedFunc(); Console.WriteLine(data); int data2 = await cc.CachedFunc(); Console.WriteLine(data2); }; // 調用委托 act(); Console.Read(); }
上面的代碼,我們連續調用了2次,第一次,等待了5秒出現結果.第二次則沒有等待直接出現結果和預期的效果一致.
9.Numeric literal syntax improvements(數值文字語法改進)
這個就純粹的是..為了好看了.
在C#7.0中,允許數字中出現"_"這個分割符號.來提高可讀性,舉例如下:
int a = 123_456; int b = 0xAB_CD_EF; int c = 123456; int d = 0xABCDEF; Console.WriteLine(a==c); Console.WriteLine(b==d); //如上代碼會顯示兩個true,在數字中用"_"分隔符不會影響結果,只是為了提高可讀性
當然,既然是數字類型的分隔符,那么 decimal
, float
和 double 都是可以這樣被分割的..
C#7.1
異步Main函數
最讓測試異步代碼的開發人員沮喪的,無疑是控制台應用當前不支持異步入口點(EntryPoint)。雖然變通方法是編寫多行樣板代碼,
但是這樣的模式依賴於對方法的非正常使用,難於理解。例如:
public static void Main() { MainAsync().GetAwaiter().GetResult(); } private static async Task MainAsync() { ... // 程序主代碼。 }
為解決這個問題,在“異步Main函數建議”中,添加了如下四個新的函數簽名,羅列了可能的入口點。
static Task Main() static Task Main() static Task Main(string[]) static Task Main(string[])
如果代碼中不存在另一個非異步Main函數,那么只要給出一個上述的入口點函數,編譯器就會生成所需的樣板代碼。唯一的限制是需要向后兼容。
Microsoft曾考慮允許“async void Main()”,但是這種做法會使編譯器更復雜,並且Microsoft總體上並不鼓勵在事件處理器之外使用“async void”。
默認值(即Nothing)
VB沒有表示“null”的關鍵字,這是C#和VB間的一個微妙的差別。但是VB有一個關鍵字“Nothing”。在語言技術規范中,對該關鍵字給出了如下說明:
Nothing是一個特殊的常值。它沒有類型,可轉換為類型系統中的任意類型,也包括類型參數。在轉換為某個特定類型后,它等價於該類型的默認值。
C#當前使用“default(T)”模式實現同一效果,但略為繁瑣,尤其是類的名字很長時。C# 7.1中將提供一個“默認常值”(Default Literal),其描述為:
這一類型的表達式可通過常值轉換為默認值或null值,隱式地轉換為any類型。
該類型向默認常值的推理與向null常值推理的工作機制一樣,除非允許any類型(不只是引用類型)。
在可以使用null的地方,通常也可以使用默認常值。這一做法被看成是C#建議中的一個倒退,可能因為人們通常會對兩個非常類似的方法完成同一件事大皺眉頭。在設計會議紀要中,就有人提出疑問:
我們是否正在挑起類型之爭?
一個使用默認常值的例子如下:
ImmutableArray x = default; return default; void Method(ImmutableArray arrayOpt = default) var x = new[] { default, ImmutableArray.Create(y) }; const int x = default; if (x == default) if (x is default) y = default as RefType //編譯器告警:總是null。 int i = default
下面例子給出的是對默認常值的非法使用:
const int? y = default; if (default == default) if (default is T) var i = default throw default
后者無疑是一個C#設計上的奇特構件。在設計會議紀要中,給出了如下說法:
在C#中,允許開發人員拋出null。這會引發一個運行時錯誤,進而導致拋出一個NullReferenceException異常。因此,拋出NullReferenceException並非正大光明的,而是一種丑陋的模式。
完全沒有理由允許拋出默認值。我們並不認為用戶會感覺這是可行的,或是了解它的工作機制。
Microsoft並未引入默認常值,而是考慮通過擴展“null”實現同一效果。因為在VB中“nothing”和“null”是兩個不同的關鍵詞,所以在VB中可以這樣做。即使不使用關鍵字,VB中也具有null的概念。因此,開發人員可以看到“NothingReferenceException”這樣的異常。
在C#中,開發人員可能常會有這樣的一個疑問:“null是否表示的是實際的空值,或是表示了可能為空值也可能不為空值的默認值?”我們認為,這是一個令人非常困惑的問題。
推導元組名(Infer Tuple Names)
雖然開發人員不常考慮到,但是C#中的匿名類型包括了命名推導。例如,編寫如下代碼時,對象y將具有名為A和B的屬性:
var y = new { x.A, x.B };
根據“推導元組名建議”,值元組基本具有同樣的功能。
var z1 = (A: x.A, B: x.B); //顯式名字。 var z2 = (x.A, x.B); //推導名字。
但是匿名類型和值元組間存在着一些顯著的差異:
- 匿名類型需要屬性名,屬性明可以是顯示指定的,也可以是推導得到的。
- 值元組會將未命名屬性標為Item1、Item2等。
- 如果匿名類型具有重復的名字,那么會產生編譯錯誤。
- 如果值元組具有重復的顯式名字,那么會產生編譯錯誤。
- 如果值元組具有重復的推導名字,那么推導名會被跳過。例如:(x.A, x.B, y.A)將轉化成(Item1, B, Item3)。
- 值元組不能使用如下保留名字:ToString、Rest、ItemN(N是大於0的數字)。
C#和VB間有hen一個有意思的差別,VB可以通過函數去推導匿名屬性名。例如:
var y = new { x.A, x.Bar() }; //編譯錯誤 Dim y = New With {x.A, x.Bar()} //匿名類型{A,Bar}
該功能特性將擴展適用於VB元組。
但如果恰巧有一個擴展方法使用了與推導屬性一樣的名字,這一特性就會引發破壞性更改。在建議中進一步提出:
考慮到這一更改的破壞性有限,並且在C# 7.0中,交付元組的時間窗很短,兼容性委員會認為這種破壞性更改是可以接受的。
考慮泛型約束的元組名
如果存在元組名不匹配的問題,那么編譯器會盡量警告編程人員。例如:
public static (int A, int B) Test1((int A, int B) a) Test1((A: 1, B: 2)); Test1((X: 1, Y: 2)); //給出警告,元組名不匹配。
如果開始采用泛型約束,代碼就不工作了:
public static T Test2(T a) where T : IEnumerable<(int A, int B)> Test2(new List<(int A, int B)>()); Test2(new List<(int X, int Y)>()); //沒有警告。
當給出前的解釋是,在泛型約束的條件下,編譯器是不會去檢查元組名的。理論上講,編譯器是可以捕獲這類問題的,但是所付出的性能上的代價要遠高於所得到的收益。
使用泛型的模式匹配
模式匹配是C# 7.0中新提供的特性。但是使用該特性時,存在設計上的缺陷。讓我們看一下Alex Wiese給出的如下代碼:
class Program { static void Main(string[] args) {} public void Send(T packet) where T : Packet { if (packet is KeepalivePacket keepalive) { // 使用keepalive的功能代碼。 } switch (packet) { case KeepalivePacket keepalivePacket: // 使用keepalivePacket的功能代碼。 break; } } } public class Packet {} public class KeepalivePacket : Packet {}
代碼會報如下錯誤:“An expression of type T cannot be handled by a pattern of type KeepalivePacket.”。但如果我們將參數改為System.Object類型,而不是T類型,代碼就工作正常了。
public void Send(object packet)
C# 7.1,通過對引發模式匹配的規則進行微調,修正了這一問題。
我們改進了“模式匹配技術規范”中的一段內容,下面以粗體標出了我們所建議添加的內容:
我們認為左側(left-hand-side)靜態類型的特定組合與特定類型是不兼容的,這會導致編譯時錯誤。我們稱靜態類型E的值與類型T是模式兼容的,如果存在標識轉換(Identity Conversion)、隱式引用轉換(Reference Conversion)、裝箱轉換(Boxing Conversion)、顯式引用轉換,或者存在從E到T的拆箱轉換(Unboxing Conversion),或者E或T均為開放類型(Open Type)。如果具有類型E的表達式與其所匹配的類型模式中的類型並不模式兼容,就會產生編譯時錯誤。
這被認為是一個軟件問題修復問題。由於該更新是“向前不兼容”的,因此只有將編譯器設為C# 7.1,才能使用這一更新。
C# 7.1/7.2:default字面量
default
字面量旨在減少一些樣板代碼。下面是一個常見的例子:
public Task<Order> GetOrderAsync(int orderKey, CancellationToken token = default(CancellationToken))
這多少有點啰嗦,因此,模仿Visual Basic的Nothing
關鍵字,上述代碼可以寫成下面這樣:
public Task<Order> GetOrderAsync(int orderKey, CancellationToken token = default)
這行代碼可以按照預期方式運行。但是,當使用一個可空的值類型時,問題就來了。
public Task<Order> GetOrders(int? limit = default)
這行代碼應該把limit
參數置為空,但在C# 7.1中,它實際返回0。
這個問題的修復計划在C# 7.2中進行,該版本會隨Visual Studio 15.5一起發布。
C# 7.1:元組名稱推斷
自從引入了匿名類型,C#就可以隱式命名屬性。例如,在下面這行代碼中,對象y
會擁有名為A
和B
的屬性。
var y = new { x.A, x.B };
在C# 7.1中,值元組也具有這個特性。
-
var z1 = (A: x.A, B: x.B); //顯式名稱
-
var z2 = (x.A, x.B); //推斷名稱
要了解更多有關元組名稱推斷的信息,請看下我們之前的報道。
C# 7.1:Async Main
這里沒有多少可說的。Main函數現在可以異步執行,這減少了之前需要編寫的一些樣板代碼。
C# 7.2:條件Ref
C#的條件操作符通常被稱為“三元運算符”,因為這是這門語言中的唯一一個。C# 7.2將會提供第二個三元操作符,名為條件Ref操作符。
這個小特性讓開發人員可以在條件中使用ref
表達式。下面是提案中的一個例子:
ref var r = ref (arr != null ? ref arr[0]: ref otherArr[0]);
注意,除了在靠近兩種可能結果的地方需要使用ref
關鍵字外,在包含整個表達式的括號外也需要使用ref
關鍵字。
C# 7.2:起始分隔符
該特性擴展了在數值字面量中使用下划線的能力。下面的示例摘自提案:
-
123 // C # 1.0及更高版本可用
-
-
1_2_3 // C # 7.0及更高版本可用
-
0x1_2_3 // C # 7.0及更高版本可用
-
0b101 // C # 7.0新增的二進制字面量
-
0b1_0_1 // C # 7.0及更高版本可用
-
-
// 在C # 7.2中,_可以用在`0x`或`0b`之后
-
0x_1_2 // C # 7.2及更高版本可用
-
0b_1_0_1 // C # 7.2及更高版本可用
C# 7.2:非尾部命名參數
C#中的命名參數服務於兩種目的:
- 允許跳過可選參數;
- 明確訪問接口,尤其是
Boolean
參數。
該特性處理第二種情況。例如:
-
void DoSomething(bool delayExecution, bool continueOnError, int maxRecords);
-
DoSomething( true, false, 100);
除非開發人員記住了函數簽名,否則很難一眼就看出了true
和false
對應什么。過去,開發人員可以寫成下面這樣:
DoSomething(delayExecution: true, continueOnError: false, maxRecords: 100);
但是,如果對maxRecords
參數沒有疑問卻還需要指定似乎就有點奇怪。在非尾部命名參數提案中,開發人員可以根據需要指定參數。
DoSomething(delayExecution: true, continueOnError: false, 100);
編者注:當清晰度成為問題時,Enum
仍然好於Boolean
。
C# 7.2:Private Protected
C#有5個訪問級別:private
、internal
、protected
、protected
或internal
、public
。但是,CLR還有第六個訪問級別,名為FamANDAssem
,“允許程序集中的子類型訪問”。
冷知識:在CLR中,protected
稱為family
,而internal
稱為assembly
。
借助新關鍵字“private protected
”,開發人員可以使用CLR的FamANDAssem
標識了。Private Protected提案說明了這樣做的重要性:
在許多情況下,API都會包含一些成員函數,只打算讓提供該類型的程序集中的子類實現並使用。CLR提供了用於此目的的訪問級別,但C#中沒有。因此,別無選擇,API所有者要么訴諸於
internal
保護、自律或自定義分析器,要么使用protected
,並提供額外的文檔說明,雖然該類型的公開文檔中有這個成員函數,但它並不是公有API的一部分。至於后者的例子,可以看下RoslynCSharpCompilationOptions
中以Common開頭的成員。
C# 7.2:只讀引用
我們之前報道過只讀引用,所以這里沒什么新東西要介紹。本質上講,只讀引用只是為了說明開發人員希望通過引用傳遞結構從而獲得性能收益,而不是真正改變值的能力。
目前,只讀引用提案尚處於原型階段,還沒有實現。
ref-like類型編譯時安全強化[7.2提案]
該C#特性又稱為“內部指針”或“ref-like
類型”。該提案旨在讓編譯器可以要求特定的類型(Span<T>
)僅出現在棧上。該特性僅對高性能場景而言比較重要。從我們上次報道以來,ref-like類型提案沒有任何變化。
放棄的特性
以下特性沒有被標記為7.2提案的一部分。雖然這不是說一定不會標記,但可能不會很快發生。
原文鏈接:https://blog.csdn.net/paxhujing/article/details/52290017