1. 程式人生 > 實用技巧 >C#高階程式設計第11版 - 第六章 索引

C#高階程式設計第11版 - 第六章 索引

【1】6.2 運算子

1.&符在C#裡是邏輯與運算。管道符號|在C#裡則是邏輯或運算。%運算子用來返回除法運算的餘數,因此當x=7時,x%5的值將是2。

【2】6.2.1 運算子的簡寫

1.下面的例子++運算子來演示字首式和字尾式之間的不同表現:

int x = 5;
if (++x == 6) // true – x先自加,再進行判斷,此時x為6,因此為true。
{
    Console.WriteLine("This will execute");
} 
if (x++ == 7) // false – x先判斷是否等於7,此時x為6,不等於7,所以為false,最後x自加,變成7
{
    Console.WriteLine(
"This won't"); }

【3】6.2.2 條件表示式運算子 ( ? : )

條件表示式運算子 ( ? : ),也被成為三元運算子,是if...else程式碼段的速寫方式。

語法如下所示:

condition ? true_value : false_value

【4】6.2.3 checked和unchecked

1.checked。CLR將會強制進行資料溢位檢查,一旦發生溢位,就會直接丟擲一個OverflowException。正如下面的例子所示:

byte b = 255;
checked
{
    b++; // System.OverflowException: Arithmetic operation resulted in an overflow.
} Console.WriteLine(b);

也可以在VS專案屬性設定是否要進行算數運算的溢位檢查。

2.如果你想忽略溢位檢查,你可以將指定程式碼段標記為unchecked:

byte b = 255;
unchecked
{
    b++;
} 
Console.WriteLine(b);

unchecked是預設操作。只有當你在一個顯式標記為checked的大程式碼段內需要忽略某些資料溢位的時候,你才需要顯式指定unchecked。

3.預設情況下,上下限溢位並不會被檢查因為它對效能有影響。當你為你的專案使用預設的check操作時,每個算數運算的結果都會被確認,無論它是否會有溢位。即使是for語句裡常見的i++語句也不例外。為了不過度的影響程式效能,將溢位檢查設定為預設不檢查是更好的方案,你只需要在需要的地方使用checked運算子即可。

【5】6.2.4 is運算子

1.is運算子允許你檢查某個例項是否相容(compatible)某個指定型別。可以用is來檢查常量,型別和變數。

2.通過使用is運算子,判斷型別是否匹配的時候,可以在型別的右側宣告一個變數。假如is運算子的表示式計算結果為true,變數將會指向型別的例項。

public static void AMethodUsingPatternMatching(object o)
{
    if (o is Person p)
    {
        Console.WriteLine($"o is a Person with firstname {p.FirstName}");
    }
} 
//...
AMethodUsingPatternMatching (new Person("Katharina", "Nagel"));

【6】6.2.5 as運算子

1.as運算子用在引用型別上,用來顯示地進行型別轉換。如果某個物件可以轉換成指定型別,as就會成功執行。假如型別不匹配,as運算子將會返回一個null值。

2.as操作符允許你在一步內執行安全的型別轉換,而不用事先通過is運算子進行判斷再進行型別轉換。

【7】6.2.6 sizeof運算子

1.通過sizeof運算子來決定一個值型別在棧裡儲存的記憶體大小(以byte為單位):

Console.WriteLine(sizeof(int));

上面這句程式碼將會輸出4,因為int是4位元組長度。

2.假如struct值擁有值型別變數的時候,你也可以對struct使用sizeof運算子。

3.不能將sizeof運算子用在class上。

4.預設情況下是不允許書寫不安全的程式碼的,你需要在"專案->生成"中勾選上"允許不安全程式碼",或者在csproj專案檔案中新增上<AllowUnsafeBlocks>標籤,設定為true。

5.為了在程式碼中使用sizeof,你需要顯式宣告一個unsafe程式碼塊:

unsafe
{
    Console.WriteLine(sizeof(Point));
}

【8】6.2.7 typeof運算子

typeof運算子將會返回一個System.Type型別的例項用來代表指定型別。例如,typeof(string)返回的是一個Type物件,用來代表System.String型別。這點在你通過反射技術從一個物件中動態查詢指定資訊時非常有用。

【9】6.2.8 nameof運算子

1.這個運算子接收一個識別符號,屬性或者方法,返回相應的名稱。當你需要用到一個變數名稱的時候:

public void Method(object o)
{
    if (o == null) throw new ArgumentNullException(nameof(o));
}

2.簡單地使用字串名稱的時候,假如你拼錯了某個單詞,是不會引起任何編譯錯誤的。這個nameof運算子則可以解決這個問題。

【10】6.2.9 index運算子

用到索引運算子(中括號)來訪問陣列:

int[] arr1 = {1, 2, 3, 4};
int x = arr1[2]; // x == 3

【11】6.2.10 可空型別與運算子

1.值型別和引用型別中一個很重要的區別就是引用型別可以為null值。

2.每個結構體都可以被定義為可空型別,就像long?DateTime?

long? l1 = null;
DateTime? d1 = null;

3.假如你在程式碼中使用了可空型別,你必須時刻考慮可空型別面對不同運算子時null值的影響。通常,當使用一元(unary)或者二進位制運算子配合可空型別進行運算時,只要有一個運算元為null的話,結果往往也為null:

int? a = null;
int? b = a + 4; // b = null
int? c = a * 5; // c = null

4.當對可空型別進行比較時,假如只有一個運算元為null,則比較結果為false。這意味著某些時候你不需要考慮結果為true的可能,只需要考慮它的反面肯定是false的情況即可。這種情況經常發生在和正常型別進行比較時。舉個例子,在下面的例子裡,只要a為null值,則else語句總是為true的不管b就是是+5還是-5。

int? a = null;
int? b = -5;
if (a >= b) // if a or b is null, this condition is false
{
    Console.WriteLine("a >= b");
} 
else
{
    Console.WriteLine("a < b");
}

5.可能為null值意味著你不能簡單地在一個表示式裡同時使用可空型別和非可空型別。

6.當然你使用在C#的型別定義後面使用關鍵字?,如int?,編譯器將這個型別處理成泛型型別Nullable<int>。編譯器將簡寫的方式替換成泛型型別減少型別轉換的開銷。

【12】6.2.11 空值合併運算子 ( ?? )

1.空值合併(coalescing)運算子 ( ?? )提供了一種簡寫的方式,來滿足處理可空型別或者引用型別為null值時的情況。這個運算子需要兩個運算元,第一個運算元必須是可空型別或者引用型別,第二個運算元必須是第一個運算元同樣的型別或者可以隱式轉換成第一個運算元的型別。

2.合併運算子將按照以下規則進行求值:

  • 假如第一個運算元不為null,那麼整個表示式的值就是第一個運算元的值。
  • 假如第一個運算元是null,則整個表示式將會以第二個運算元的值為準。

【13】6.2.12 空值條件運算子 ( ?. 和?[] )

1.使用空值條件運算子來訪問FirstName屬性的時候(p?.FirstName),當p為null時,只有null值會返回而不會繼續執行右側的表示式:

public void ShowPerson(Person p)
{
    string firstName = p?.FirstName;
    //...
}

當一個int型別的屬性要使用空值條件運算子的時候,它的結果不能直接賦值給一個普通的int型別因為結果可能會為空,一個解決方案是賦值給可空int型別int?

int? age = p?.Age;

當然你也可以使用空值合併運算子來解決這個問題,通過定義一個預設值(例如0)來應付萬一左側的表示式為null的情況:

int age1 = p?.Age ?? 0;

2.多個空值條件運算子也可以被合併。這裡有一個Person物件,我們要訪問它的Address屬性,而這個屬性又包含一個City屬性,我們可能會先檢查Person物件是否為空,然後再檢查Address屬性是否為空,都不為空的情況下我們才能訪問到City屬性:

Person p = GetPerson();
string city = null;
if (p != null && p.HomeAddress != null)
{
    city = p.HomeAddress.City;
}

而當你使用空值條件運算子的時候,你就可以簡單地這麼寫:

string city = p?.HomeAddress?.City;

3.通過使用?[]來訪問陣列。如下所示,假如arr陣列為null,則不會接著執行[0]訪問arr的第一個陣列元素,直接就返回一個null值,並且這裡使用了空值合併運算子??,當左側的表示式返回為null時,我們將右側的0賦值給x1:

int x1 = arr?[0] ?? 0;

【14】6.2.13 運算子的優先順序和關聯性

1.最典型的從右向左運算的例子就是賦值運算子。例如下面的程式碼,我們首先是將z的值賦值給y,然後將y的值再賦給x:

x = y = z

2.另外一個容易被誤導的右聯運算子是條件表示式運算子( ? : ),下面兩個表示式是等價的:

a ? b : c ? d: e
a ? b : (c ? d: e)

【15】6.3 使用二進位制運算子

一個按位與,按位或,按位異或,取反的二進位制示例。

【16】6.3.1 移位

二進位制每往左移動一位,相當於值乘以2。

【17】6.3.2 有符號數和無符號數

當你使用有符號型別,如int,long,short來儲存二進位制數時,它的左邊第一位是用來代表符號的。當你使用int時,它所能代表的最大值為2147483647——也就是31位的1(二進位制)或者0x7FFF FFFF。而使用uint的話,則最大值可以是4294967295——32位的1(二進位制)或者0xFFFF FFFF。

【18】6.4.1 型別轉換

兩個轉換機制——隱式(implicit)和顯式(explicit)。

【19】6.4.1.1 隱式轉換

1.可以隱式地將較小的資料型別轉換成較大的資料型別,反過來則不行。

2.

可空資料型別在隱式轉換為值型別的時候需要考慮更多一些:

  • 可空資料型別轉換成其他可空型別就跟上面表格中的普通型別一樣,譬如int?可以隱式地轉換成long?float?double?decimal?
  • 普通型別隱式地轉換成可空型別也與上面表格中定義的規則比較類似,譬如int型別可以隱式地轉換成long?float?double?decimal?
  • 可空型別不能隱式地轉換成普通型別。你必須使用顯式轉換。這是因為可空型別有可能代表null值,而普通型別無法儲存null值。

【20】6.4.1.2 顯式轉換

1.你可以使用強制型別運算子來顯式地處理這些轉換。當你使用強制轉換運算子時,你故意(deliberately)強制編譯器進行此種轉換,就像下面這樣:

long val = 30000;
int i = (int)val; // 有效的強制轉換,因為int的最大值為2147483647

2.可以使用checked運算子來保證強制轉換的安全並且強制使執行時在溢位時丟擲一個OverflowException異常:

long val = 3000000000;
int i = checked((int)val); // System.OverflowException: Arithmetic operation resulted in an overflow

3.在下面這段程式碼中,price的值增加了0.5,並且最後結果被強制轉換為int型別:

double price = 25.30;
int approximatePrice = (int)(price + 0.5);

在這種轉換中,小數部分的資料丟失了——換句話說,就是小數點之後的內容都丟失了。

4.試圖將一個無符號整數轉換成char型別時的情況:

ushort c = 43;
char symbol = (char)c;
Console.WriteLine(symbol); // +

5.轉換陣列元素,將它轉換成某個結構體的成員變數:

struct ItemDetails
{
    public string Description;
    public int ApproxPrice;
} 
//
double[] Prices = { 25.30, 26.20, 27.40, 30.00 };
ItemDetails id;
id.Description = "Hello there.";
id.ApproxPrice = (int)(Prices[0] + 0.5);

6.將一個可空型別轉換成普通型別或者另外一個可空型別可能會引起資料丟失,因此你需要使用顯式強制轉換。即使將可空型別強制轉換成它的內建型別時也是這樣的——例如int?轉int或者float?轉float的時候。這是因為可空型別可能存在null值,它不可能用普通型別來表示。只要在兩個不相同的普通型別之間可以進行強制轉換,那麼相應的可空型別之間也可以進行強制轉換。此外,當試圖將一個值為null的可空型別強制轉換為普通型別時,將會丟擲一個InvalidOperationException,就像下面這樣:

int? a = null;
int b = (int)a; // Nullable object must have a value.

值不為null或許就不會有InvalidOperationException?

7.就值型別來說,你只能在數值型別之間互相轉換,或者與char和enum之間轉換。你無法將Boolean直接轉換成任何型別,反之亦然。

8.假如你需要將一個字串轉換成一個數值型或者布林值,你可以使用預定義型別內建的Parse方法:

string s = "100";
int i = int.Parse(s);
Console.WriteLine(i + 50); // Add 50 to prove it is really an int

注意當Parse方法無法轉換一個字串時,它會丟擲一個異常(例如你想將"Hello"轉換成一個整數的時候)。

【21】6.4.2 裝箱和拆箱

1.這個從值型別轉換成引用型別的操作術語,就叫做裝箱。基本來講,執行時為所需的object物件建立了一個臨時的引用型別盒子,並存儲在託管堆上。這個轉換是隱式的。

2.拆箱則是用來描述引用型別轉值型別操作。這個操作必須是顯式地。和我們曾經介紹過的強制轉換很類似。

3.當拆箱的時候,你要小心拆箱後的型別是否容得下被裝箱的原始型別。舉個例子:C#的int型別,只能儲存4位元組的資料,假如你拆箱一個long值(8位元組),賦值給一個int,像下面這樣,同樣會提示InvalidCastException,這點與long變數的實際值是否在int取值範圍內無關:

long myLongNumber = 1;
object myObject = (object)myLongNumber;
int myIntNumber = (int)myObject; // Unable to cast object of type 'System.Int64' to type 'System.Int32'

myLongNumber

【22】6.5 比較物件是否相等

物件之間是否相等完全取決於你是比較兩個引用型別(類的例項之間)或者是值型別(如基礎資料型別,struct例項,或者列舉型別)。

【23】6.5.1 比較引用型別是否相等

1.System.Object定義了3個不同的方法用來比較物件是否相等:一個ReferenceEquals方法,一個靜態Equals方法以及一個例項Equals虛方法。你也可以實現介面IEquality<T>,它定義了一個Equals方法,帶有一個代替object型別的泛型型別引數。除了這些之外,你還可以使用比較運算子==

2.ReferenceEquals是一個靜態方法,用來測試是否兩個引用都指向同一個類的例項,具體來說就是兩個引用是否具有同一個記憶體地址。

3.作為一個靜態方法,它無法被重寫,所以System.Object類裡的這個方法就是唯一的ReferenceEquals版本。當兩個引用指向的是同一個物件例項時,ReferenceEquals會返回true,否則返回false。

4.null和null值也為true。

5.System.Object裡實現了一個的Equals虛方法,也可以用來比較引用。

6.因為這個方法宣告成了virtual,因此你也可以在你自己的類裡面重寫它,用來按照你自己的需要進行比較。尤其是,當你將你自己的類作為字典的鍵值的時候,你必須重寫這個方法,以便能進行值的比較。另外,取決於你如何重寫Object.GetHashCode,包含你自己類物件的字典可能會完全沒法用或者非常低效。

7.注意當你重寫Equals方法的時候,你的實現必須不會出現任何異常。進一步講,如果你重寫的Equals出異常將會引起一些其他的問題,因為不單單只是將你的類應用在字典中時會出問題,一些其他的.NET基礎類也會內部呼叫到這個方法。

8.靜態Equals方法和例項版本的Equals方法實際上乾的事都一樣。唯一的區別就是靜態版本的Equals方法帶有兩個引數並且比較這兩個引數是否相當,例項版本就一個引數而已。這個方法也可以用來處理兩個引數都是null的清況。因此,它提供了額外的安全機制來確保當某個引數可能為null的時候不丟擲異常。

9.靜態Equals方法首先判斷傳遞過來的是不是null,假如兩個引數都是null,返回true(因為null被認為是與null相等的)。假如只有其中一個是null,則返回false。假如兩個引用都有值,則呼叫例項版本的Equals方法。這意味著當你在自己的類中重寫了Equals方法的話,效果跟你重寫了靜態版本一樣。

10.比較運算子( == )。最好思考一下比較運算子作為嚴格值比較和嚴格物件比較之間的中間選項。

【24】6.5.2 比較值型別是否相等

1.ReferenceEquals用來比較引用,Equals用來比較值,而==運算子則是折衷方案(intermediate case)。

2.你自己建立的結構體,預設不支援==運算子過載。直接寫sA == sB會導致一個編譯錯誤,除非你在自己的程式碼裡提供了==的過載。列sA和sB都是結構體的例項。

3.Microsoft已經在System.ValueType類過載了例項Equals方法,來測試值型別的相等性。

4.對值型別呼叫ReferenceEquals經常會返回false,這是因為呼叫這個方法的時候,值型別會被裝箱成object型別。原因是這裡分別進行了兩次裝箱,這意味著你獲得的是兩個不同的引用。因此,對於值型別來說沒有必要呼叫ReferenceEquals進行比較因為它沒有任何意義。

5.假如一個值型別包含引用型別作為欄位,你可能想通過重寫Equals方法為這些引用型別的欄位提供更合適的應用場景,因為預設的Equals方法僅僅會簡單地比較它們的記憶體地址。

【25】6.6 運算子過載

1.你不想經常呼叫某個類的屬性或方法時,使用運算子過載會很有用。

2.過載不單單僅限於算數運算子。你同樣需要考慮比較運算子,如==<!等等。

3.對於結構體struct來說,==運算子預設是無效的。嘗試比較兩個結構體是否相等只會產生一個編譯錯誤,除非你顯式地過載了==來告訴編譯器如何實現這個比較。

4.合理使用運算子過載使你能夠寫出更具可讀性並且更直觀的程式碼。

【26】6.6.1 運算子的工作方式

1.int+uint=long+long

2.浮點數是通過尾數(mantissa)和指數(exponent)進行儲存的。對它們進行相加將會包含位移動(bit-shifting)操作,以便兩數擁有相同的指數,然後再將尾數進行相加,對結果的尾數進行轉換,使得結果能包含最高的精度。

3.double+int=double+double

4.假如編譯器能找到合適的過載,它將會呼叫運算子的實現。假如它找不到,它會檢查是否有其他的+過載適合處理這種情況——可能有某些其他的型別實現了+過載,引數雖然不是Vector,但可以隱式地轉換成Vector型別。假如編譯器還是不能找到合適的過載方法,它將會丟擲一個編譯錯誤,因為它找不到任何過載方法可以處理這個操作。

【27】6.6.2 運算子過載的示例:Vector 結構

1.標量(scalar)。

2.在struct或者class裡過載運算子並沒有什麼兩樣。

3.運算子過載,為Vector類提供了+運算:

public static Vector operator +(Vector left, Vector right) => new Vector(left.X + right.X, left.Y + right.Y, left.Z + right.Z);

運算子的過載和靜態方法很像,除了多了一個operator關鍵字以外。這個關鍵字告訴編譯器你定義了一個運算子過載。operator關鍵字後緊跟相關運算子的符號,這裡我們用的是加法符號+。返回型別為你使用這個運算子後最終得到的型別,因為我們知道兩個向量相加還是向量,因此返回型別為Vector。在這個特殊的加法過載例子中,返回型別與它的容器型別一致,但這並不是必然的,後續你可以看到返回值為其他型別的例子。兩個引數left和right是你要用來做加法運算的運算元。對於二元運算子來說,如加法和減法,第一個引數是在運算子左邊的,第二個引數則是在運算子右邊。

4.向量與標量之間的乘法運算非常簡單,就是向量的每個部分都單獨地與標量進行乘法運算,如下所示:

public static Vector operator *(double left, Vector right) => new Vector(left * right.X, left * right.Y, left * right.Z);

5.注意運算子過載方法引數的位置,要相對應才不會出錯。

6.數學運算子的過載並非只能返回所在類的型別。

7.C#要求所有的運算子過載必須宣告成public和static,這意味著它們跟類或者結構體相關,而非具體的例項。因此,運算子過載的方法體無法直接訪問非靜態的內部成員或者使用this關鍵字。

8.+=雖然看上去是一個運算子,但其實它的計算分兩步走:先進行加法運算,再進行賦值。跟C++不同的是,C#不允許你過載=運算子,但如果你過載了+運算子,編譯器會自動應用你的+過載來實現+=操作。同樣的原理也適用於其它的賦值運算子,譬如-=*=等等。

【28】6.6.3 比較運算子的過載

1.C#要求你成對地實現比較運算子,換句話說,假如你實現了==的過載,你就必須也實現!=,否則你將會得到一個編譯錯誤。if(){}else{}

2.比較運算子的返回型別必須是bool型別。

3.請不要嘗試在你過載的==中只簡單地呼叫System.Object中例項版本的Equals方法來返回true或者false。假如你這麼做了,當代碼中試圖進行(objA == objB)的比較時,有可能會報錯。因為objA可能是null值,編譯器實際上是試圖執行null.Equals(objB)方法。你可以通過override例項版本的Equals方法來實現相等性的比較,這更安全一些。

4.對於值型別來說,你還必須實現IEquatable<T>介面,比起基類object定義的Equals虛方法來說,這個介面定義的是強型別的版本,基於上面我們已經實現過的程式碼,你可以很簡單地實現它:

private readonly struct Vector:IEquatable<Vector>
{
    //...
    public bool Equals([AllowNull] Vector other) => this == other;
}

【29】6.6.4 可以過載的運算子

1.算術二元運算子,算術一元運算子,位二元運算子,位一元運算子,比較運算子,賦值運算子,索引器,型別強制轉換。

2.你可能會對過載true和false運算子可以過載感到疑惑。事實上,一個整數值能否代表true或者false完全取決於你所使用的技術或者框架。在很多技術中,0代表著false而1代表著true;而有些技術則定義0為false,非0則為true。當你為某個型別實現了true或者false過載,那麼該型別的例項直接就可以作為條件語句的判斷條件。

【30】6.7 實現自定義的索引運算子