天天看點

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

點選檢視第一章

第2章

C#語言基礎

本章将介紹一些C#語言的基礎知識。

本章和接下來的兩章中的所有程式和代碼片段都可以作為互動式示例在LINQPad中運作。閱讀本書時使用這些示例可以加快你的學習進度。在LINQPad中編輯執行這些示例可以立即看到結果,無須在Visual Studio中建立項目和解決方案。

若要下載下傳這些示例,請點選LINQPad中的Samples頁籤,然後點選“Download more samples”。LINQPad是免費程式,詳見

http://www.linqpad.net

2.1 第一個C#程式

以下程式計算12乘以30,并将結果360列印到螢幕上。雙斜線“//”表示其後的内容是注釋:

using System;                     // Importing namespace

class Test                        // Class declaration
{
  static void Main()              // Method declaration
  {
    int x = 12 * 30;              // Statement 1
    Console.WriteLine (x);        // Statement 2
  }                               // End of method
}                                 // End of class
           

該程式的核心是以下兩個語句:

int x = 12 * 30;
Console.WriteLine (x);           

在C#中,語句按順序執行,每個語句都以分号(或者代碼塊,詳見本章後續内容)結尾。第一個語句計算表達式12*30的值,并把結果存儲到一個局部變量x中,該變量是一個整數類型。第二個語句調用Console類的WriteLine方法,将變量x的值輸出到螢幕上的文本視窗中。

方法(method)是由一系列語句(語句塊)組成的行為。語句塊由一對大括号,及其中的零個或者多個語句組成。示例中定義了一個名為Main的方法:

static void Main()
{
  ...
}           

編寫高層函數來調用低層函數可令程式得到簡化。可重構(refactor)該程式,使用一個可重用的方法來計算某個整數乘以12的結果:

using System;

class Test
{
  static void Main()
  {
    Console.WriteLine (FeetToInches (30));      // 360
    Console.WriteLine (FeetToInches (100));     // 1200
  }

  static int FeetToInches (int feet)
  {
    int inches = feet * 12;
    return inches;
  }
}           

方法可以通過參數來接受調用者輸入的資料,并通過指定的傳回類型向調用者傳回輸出資料。上述代碼中定義了一個FeetToInches方法,該方法有一個用于輸入英尺的參數和一個用于輸出英寸的傳回類型:

static int FeetToInches (int feet ) {...}           

示例中的字面量30和100是傳遞給FeedToInches方法的實際參數(argument)。而Main方法後的括号中是空的,因而沒有任何參數。其傳回類型是void說明它不向調用者傳回任何值:

static void Main()           

C#将Main方法作為程式執行的預設入口點。Main方法也可以傳回整數值(而非void)進而将其傳回給程式的執行環境(非0傳回值往往代表一個錯誤)。Main方法還可以接受一個字元串數組作為參數(數組中包含了傳遞給可執行程式的任何實際參數)。例如:

static int Main (string[] args) {...}           

數組(例如string[])是固定數量的某種特定類型元素的集合。數組由元素類型和它後面的方括号指定。相關内容将在2.7節介紹。

方法是C#中的諸多種類的函數之一。另一種函數是我們用來執行乘法運算的*運算符。其他的函數種類還包括構造器、屬性、事件、索引器和終結器。

本例将兩個方法組合到一個類中。類由函數成員和資料成員組成,并形成面向對象的構件塊。Console類将處理指令行輸入/輸出功能的成員,例如WriteLine方法,聚集在一起。Test類則由Main方法和FeetToInches兩個方法組成。類是類型之一,我們将在2.3節中介紹它。

程式的最外層将類型組織到了命名空間中。為了使System命名空間在應用程式中生效,并能夠使用Console類,需要使用using指令。應将所有的類定義在TestPrograms命名空間中,例如:

using System;

namespace TestPrograms
{
  class Test  {...}
  class Test2 {...}
}           

.NET Framework由若幹嵌套的命名空間組織而成。例如,以下命名空間中包含處理文本的類型:

using System.Text;           

使用using指令僅僅是為了友善;也可以使用命名空間加類型名稱(例如System.Text.StringBuilder)這種完整限定名稱來引用類型。

編譯

C#編譯器将一系列.cs擴充名的源代碼檔案編譯成程式集。程式集是.NET中的最小打包和部署單元。程式集可以是一個應用程式或者是一個庫。普通的控制台程式或Windows應用程式是一個.exe檔案,包含一個Main方法。而庫是一個.dll檔案,即一個沒有入口點的.exe檔案。庫可以被應用程式或其他的庫調用(引用)。.NET Framework就是由一系列庫組成的。

C#編譯器是csc.exe。我們既可以使用像Visual Studio這樣的IDE來編譯程式,也可以在指令行中手動調用csc指令編譯C#程式(編譯器本身通過庫調用,詳情參見第27章)。如需手動編譯C#程式,首先将程式儲存成檔案(例如MyFirstProgram.cs),然後進入指令行并調用csc指令(csc位于%ProgramFiles(X86)%msbuild14.0bin)譯注1,如下所示:

csc MyFirstProgram.cs           

這個指令将生成名為MyFirstProgram.exe的應用程式。

奇怪的是,.NET Framework 4.6和4.7仍然包含C# 5的編譯器。若要使用C# 7指令行編譯器,必須安裝Visual Studio 2017或MSBuild 15。

如需生成庫(.dll),請使用如下指令:

csc /target:library MyFirstProgram.cs

我們将在第18章詳細介紹程式集。

2.2 文法

C#的文法基于C和C++文法。在本節中,我們将使用下面的程式介紹C#的文法元素:

using System;

class Test
{
  static void Main()
  {
    int x = 12 * 30;
    Console.WriteLine (x);
  }
}           

2.2.1 辨別符和關鍵字

辨別符是程式員為類、方法、變量等選擇的名字。下面按順序列出了上述示例中的辨別符:

System Test Main x Console WriteLine

辨別符必須是一個完整的詞,它由以字母和下劃線開頭的Unicode字元構成。C#辨別符是區分大小寫的。通常約定參數、局部變量以及私有字段應該以小寫字母開頭(例如myVariable),而其他類型的辨別符則應該以大寫字母開頭(例如MyMethod)。

關鍵字是對編譯器有特殊意義的名字。以下是示例中用到的關鍵字:

using class static void int

大部分關鍵字是保留的,這意味着它們不能用作辨別符。以下列出了C#的所有關鍵字:

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

2.2.1.1 避免沖突

如果希望用關鍵字作為辨別符,需在關鍵字前面加上@字首。例如:

class class  {...}      // Illegal
class @class {...}      // Legal           

@并不是辨別符的一部分,是以@myVariable和myVariable是一樣的。

@字首在調用使用其他擁有不同關鍵字的.NET語言編寫的庫時非常有用。

2.2.1.2 上下文關鍵字

一些關鍵字是上下文相關的,它們有時不用添加@字首就可以用作辨別符。它們是:

add            dynamic    in        orderby    var
ascending        equals        into        partial    when
async        from        join        remove        where
await        get        let        select        yield
by            global        nameof        set
descending        group        on        value           

使用上下文關鍵字作為辨別符時,應避免與上下文中的關鍵字混淆。

2.2.2 字面量、标點與運算符

字面量是靜态的嵌入程式中的原始資料片段。上述示例中用到的字面量有12和30。

标點有助于劃分程式結構。以下是示例中用到的标點:

{   }   ;           

大括号可将多條語句形成一個語句塊。

分号用于結束一條語句。(但語句塊并不需要分号。)這意味着語句也可以放在多行中:

Console.WriteLine
  (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10);           

運算符用于改變和結合表達式。大多數C#運算符都以符号表示,例如乘法運算符*。我們将在本章後續内容中詳細介紹運算符。在上述示例中出現的運算符有:

.  ()   *   =           

點号(.)表示某個對象的成員(或者數字字面量的小數點)。括号在聲明或調用方法時使用;空括号在方法沒有參數時使用。(本章後續還會介紹括号的其他的用途。)等号用于指派操作。(雙等号==用于相等比較,請參見本章後續内容)。

2.2.3 注釋

C#提供了兩種方式源代碼文檔:單行注釋和多行注釋。單行注釋由雙斜線開始,到本行結束為止。例如:

int x = 3;   // Comment about assigning 3 to x           

多行注釋由/*開始,由*/結束。例如:

int x = 3;   /* This is a comment that
                spans two lines */           

注釋也可以嵌入XML文檔标簽中,我們将在4.17節中介紹。

2.3 類型基礎

類型是值的藍圖。在以下示例中,我們使用了兩個int類型的字面量12和30,并聲明了一個int類型的變量x:

static void Main()
{
  int x = 12 * 30;
  Console.WriteLine (x);
}           

變量表示一個存儲位置,其中的值可能會不斷變化。與之對應,常量總是表示同一個值(後面會詳細介紹):

const int y = 360;           

C#中的所有值都是某一種類型的執行個體。值或者變量所包含的可能取值均由其類型決定。

2.3.1 預定義類型示例

預定義類型是指那些由編譯器特别支援的類型。int就是一種預定義類型,它代表一系列能夠存儲在32位記憶體中的整數集,其範圍從-231到231-1,并且它是該範圍内數字字面量的預設類型。我們能夠對int類型的執行個體執行算術運算等功能:

int x = 12 * 30;           

C#中的另一個預定義類型是string。string類型表示字元序列,例如“.NET”或者“

http://oreilly.com

” 。我們可以通過以下方式調用函數來操作字元串:

string message = "Hello world";
string upperMessage = message.ToUpper();
Console.WriteLine (upperMessage);               // HELLO WORLD

int x = 2015;
message = message + x.ToString();
Console.WriteLine (message);                    // Hello world2015           

預定義類型bool隻有兩種值:true和false。bool類型通常與if語句一起控制條件分支執行流程。例如:

bool simpleVar = false;
if (simpleVar)
  Console.WriteLine ("This will not print");

int x = 5000;
bool lessThanAMile = x < 5280;
if (lessThanAMile)
  Console.WriteLine ("This will print");
           

在C#中,預定義類型(也稱為内置類型)擁有相應的C#關鍵字。在.NET Framework中的System命名空間下也包含了很多不是預定義類型的重要類型(例如DateTime)。

2.3.2 自定義類型示例

我們能使用簡單函數來構造複雜函數,同樣也可以使用基元類型來建構複雜類型。以下示例定義了一個名為UnitConverter的自定義類型。這個類将作為機關轉換的藍圖:

using System;

public class UnitConverter
{
  int ratio;                                                 // Field
  public UnitConverter (int unitRatio) {ratio = unitRatio; } // Constructor
  public int Convert   (int unit)    {return unit * ratio; } // Method
}

class Test
{
  static void Main()
  {
    UnitConverter feetToInchesConverter = new UnitConverter (12);
    UnitConverter milesToFeetConverter  = new UnitConverter (5280);

    Console.WriteLine (feetToInchesConverter.Convert(30));    // 360
    Console.WriteLine (feetToInchesConverter.Convert(100));   // 1200
    Console.WriteLine (feetToInchesConverter.Convert(
                         milesToFeetConverter.Convert(1)));   // 63360
  }
}           

2.3.2.1 類型的成員

類型包含資料成員和函數成員。UnitConverter的資料成員是ratio字段,函數成員是Convert方法和UnitConverter的構造器。

2.3.2.2 預定義類型和自定義類型

C#的優點之一是其中的預定義類型和自定義類型非常相近。預定義int類型是整數的藍圖。它儲存了32位的資料,提供像ToString這種函數成員來使用這些資料。類似地,我們自定義的UnitConverter類型也是機關轉換的藍圖。它儲存比率資料,還提供了函數成員來使用這些資料。

2.3.2.3 構造器和執行個體化

将類型執行個體化即可建立資料。預定義類型可以簡單地通過字面量進行執行個體化,例如12或"Hello World"。而自定義類型則需要使用new運算符來建立執行個體。以下的語句建立并聲明了一個UnitConverter類型的執行個體:

UnitConverter feetToInchesConverter = new UnitConverter (12);           

使用new運算符後會立刻執行個體化一個對象,調用對象的構造器進行初始化。構造器的定義像方法一樣,不同的是方法名和傳回類型簡化為所屬的類型名稱:

public class UnitConverter
{
  ...
  public UnitConverter (int unitRatio) { ratio = unitRatio; }
  ...
}           

2.3.2.4 執行個體與靜态成員

由類型的執行個體操作的資料成員和函數成員稱為執行個體成員。UnitConverter的Convert方法和int的ToString方法就是執行個體成員的例子。在預設情況下,成員就是執行個體成員。

那些不是由類型的執行個體操作,而是由類型本身操作的資料成員和函數成員必須标記為static。Test.Main和Console.WriteLine就是靜态方法。事實上,Console類是一個靜态類,它的所有成員都是靜态的。由于Console類型無法執行個體化,是以控制台将在整個應用程式内共享使用。

我們來對比執行個體成員和靜态成員。在下面的代碼中,執行個體字段Name屬于特定的Panda執行個體,而Population則屬于所有Panda執行個體:

public class Panda
{
  public string Name;             // Instance field
  public static int Population;   // Static field

  public Panda (string n)         // Constructor
  {
    Name = n;                     // Assign the instance field
    Population = Population + 1;  // Increment the static Population field
  }
}           

下面的代碼建立了兩個Panda執行個體,先列印它們的名字,再列印總數:

using System;

class Test
{
  static void Main()
  {
    Panda p1 = new Panda ("Pan Dee");
    Panda p2 = new Panda ("Pan Dah");

    Console.WriteLine (p1.Name);      // Pan Dee
    Console.WriteLine (p2.Name);      // Pan Dah

    Console.WriteLine (Panda.Population);   // 2
  }
}           

如果試圖求p1.Population或者Panda.Name的值,則會生成一個編譯時錯誤。

2.3.2.5 public關鍵字

public關鍵字将成員公開給其他類。在上述示例中,如果Panda類中的Name字段沒有标記為公有(public)的,那麼它就是私有的,且Test類就不能通路它。将成員标記為public就是類型的通信手段:“這就是我想讓其他類型看到的,而其他的都是我私有的實作細節。”在面向對象的術語中,稱之為類的公有成員封裝了私有成員。

2.3.3 轉換

C#可以轉換相容類型的執行個體。轉換始終會根據一個已經存在的值建立一個新的值。轉換可以是隐式或顯式的:隐式轉換自動發生而顯式轉換需要強制轉換。在以下的示例中,我們把一個int隐式轉換為long類型(其存儲位數是int的兩倍);并将一個int顯式轉換為一個short類型(其存儲位數是int的一半):

int x = 12345;       // int is a 32-bit integer
long y = x;          // Implicit conversion to 64-bit integer
short z = (short)x;  // Explicit conversion to 16-bit integer           

隐式轉換隻有在以下條件都滿足時才能進行:

編譯器能確定轉換總能成功。

沒有資訊在轉換過程中丢失。注1

相對地,隻有在滿足下列條件時才需要顯式轉換:

編譯器不能保證轉換總是成功。

資訊在轉換過程中有可能丢失。

(如果編譯器可以确定某個轉換一定會失敗,那麼這兩種轉換都無法執行。包含泛型的轉換在特定情況下也會失敗,請參見3.9.11節)

以上的數值轉換是C#中内置的。C#還支援引用轉換、裝箱轉換(見第3章)與自定義轉換(請參見4.14節)。對于自定義轉換,編譯器并沒有強制要求上述規則,是以沒有良好設計的類型有可能在轉換時出現意想不到的效果。

2.3.4 值類型與引用類型

所有的C#類型可以分為以下幾類:

  • 值類型
  • 引用類型
  • 泛型參數
  • 指針類型

本節将介紹值類型和引用類型。泛型參數将在3.9節介紹,指針類型将在4.15節中介紹。

值類型包含大多數的内置類型(具體包括所有數值類型、char類型和bool類型)以及自定義的struct類型和enum類型。

引用類型包含所有的類、數組、委托和接口類型。(這其中包括了預定義的string類型。)

值類型和引用類型最根本的不同在于它們在記憶體中的處理方式。

2.3.4.1 值類型

值類型的變量或常量的内容僅僅是一個值。例如,内置的值類型int的内容是32位的資料。

可以通過struct關鍵字定義自定義值類型(參見圖2-1):

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章
public struct Point { public int X; public int Y; }           

或采用更簡短的形式:

public struct Point { public int X, Y; }           

值類型執行個體的指派總是會進行執行個體複制。例如:

static void Main()
{
  Point p1 = new Point();
  p1.X = 7;

  Point p2 = p1;             // Assignment causes copy

  Console.WriteLine (p1.X);  // 7
  Console.WriteLine (p2.X);  // 7

  p1.X = 9;                  // Change p1.X

  Console.WriteLine (p1.X);  // 9
  Console.WriteLine (p2.X);  // 7
}           

圖2-2中展示了p1和p2擁有不同的存儲空間。

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

2.3.4.2 引用類型

引用類型比值類型複雜,它由兩部分組成:對象和對象引用。引用類型變量或常量中的内容是一個含值對象的引用。以下示例将前面例子中的Point類型重新書寫,令其成為一個類而非struct(請參見圖2-3):

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章
public class Point { public int X, Y; }           

給引用類型變量指派隻會複制引用,而不是對象執行個體。這允許不同變量指向同一個對象,而值類型通常不會出現這種情況。如果Point是一個類,那麼若重複之前的示例,則對p1的操作就會影響到p2了:

static void Main()
{
  Point p1 = new Point();
  p1.X = 7;

  Point p2 = p1;             // Copies p1 reference

  Console.WriteLine (p1.X);  // 7
  Console.WriteLine (p2.X);  // 7

  p1.X = 9;                  // Change p1.X

  Console.WriteLine (p1.X);  // 9
  Console.WriteLine (p2.X);  // 9
}           

圖2-4展示了p1和p2是指向同一對象的兩個不同引用。

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

2.3.4.3 Null

引用可以指派為字面量null,表示它并不指向任何對象:

class Point {...}
...

Point p = null;
Console.WriteLine (p == null);   // True

// The following line generates a runtime error
// (a NullReferenceException is thrown):
Console.WriteLine (p.X);           

相對地,值類型通常不能有null的取值:

struct Point {...}
...

Point p = null;  // Compile-time error
int x = null;    // Compile-time error           

C#中也有一種代表值類型為null的結構,稱為可空(nullable)類型(請參見4.7節)。

2.3.4.4 存儲開銷

值類型執行個體占用的記憶體大小就是存儲其字段所需的記憶體。例如,Point需要占用8位元組的記憶體:

struct Point
{
  int x;  // 4 bytes
  int y;  // 4 bytes

}           

從技術上說,CLR用整數倍字段的大小(最大到8位元組)來配置設定記憶體位址。是以,下面的定義的對象實際上會占用16位元組的記憶體(第一個字段的7個位元組被“浪費了”):

struct A { byte b; long l; }           

這種行為可以通過指定StructLayout屬性來重寫(請參見25.6節)。

引用類型要求為引用和對象單獨配置設定存儲空間。對象除占用了和字段一樣的位元組數外,還需要額外的管理空間開銷。管理開銷的精确值本質上屬于.NET運作時實作的細節,但最少也需要8個位元組來存儲該對象的類型的鍵,以及一些諸如多線程鎖的狀态、是否可以被垃圾回收器固定等臨時資訊。根據.NET運作時是工作在32位抑或64位平台上,每一個對象的引用都需要額外的4到8個位元組。

2.3.5 預定義類型分類

C#中的預定義類型有:

  • 數值
    • 有符号整數(sbyte、short、int、long)
    • 無符号整數(byte、ushort、uint、ulong)
    • 實數(float、double、decimal)
  • 邏輯值(bool)
  • 字元(char)
  • 字元串(string)
  • 對象(object)

C#的預定義類型又稱為架構類型,它們都在System命名空間下。下面的兩個語句僅在拼寫上有所不同:

int i = 5;
System.Int32 i = 5;           

在CLR中,除了decimal之外的一系列預定義值類型屬于基元類型。之是以将其稱為基元類型是因為它們在編譯過的代碼中有直接的指令支援。而這種指令通常翻譯為底層處理器直接支援的指令。例如:

// Underlying hexadecimal representation
int i = 7;         // 0x7
bool b = true;     // 0x1
char c = 'A';      // 0x41
float f = 0.5f;    // uses IEEE floating-point encoding           

System.IntPtr以及System.UIntPtr類型也是基元類型(參見第25章)。

2.4 數值類型

表2-1中列出了C#中所有的預定義數值類型。

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章
帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

在整數類型中,int和long是最基本的類型,C#和運作時都對其有良好的支援。其他的整數類型通常用于實作互操作性或存儲空間使用效率要求更高的情況。

在實數類型中,float和double稱為浮點類型,注2并通常用于科學和圖形計算。decimal類型通常用于金融計算這種十進制下的高精度算術運算。

2.4.1 數值字面量

整數類型字面量可以使用十進制或者十六進制表示。十六進制輔以0x字首。例如:

int x = 127;
long y = 0x7F;           

從C# 7開始,可以在數值字面量的任意位置加入下劃線以友善閱讀:

int million = 1_000_000;           

C# 7還可以用0b字首使用二進制表示數值:

var b = 0b1010_1011_1100_1101_1110_1111;           

實數字面量可以用小數或指數表示,例如:

double d = 1.5;
double million = 1E06;           

2.4.1.1 數值字面量類型接口

預設情況下,編譯器将數值字面量推斷為double類型或是整數類型。

  • 如果這個字面量包含小數點或者指數符号(E),那麼它是double。
  • 否則,這個字面量的類型就是下列能滿足這個字面量的第一個類型:int、uint、long和ulong。

例如:

Console.WriteLine (        1.0.GetType());  // Double  (double)
Console.WriteLine (       1E06.GetType());  // Double  (double)
Console.WriteLine (          1.GetType());  // Int32   (int)
Console.WriteLine ( 0xF0000000.GetType());  // UInt32  (uint)
Console.WriteLine (0x100000000.GetType());  // Int64   (long)           

2.4.1.2 數值字尾

數值字尾顯式定義了字面量的類型。字尾可以是下列小寫或大寫字母:

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

一般U和L字尾是很少需要的。因為uint、long和ulong總是可以推斷出來或者從int類型隐式轉換過來:

long i = 5;     // Implicit lossless conversion from int literal to long           

從技術上講,字尾D是多餘的。因為所有帶小數點的字面量都會推定為double類型。是以可以直接在數值字面量後加上小數點:

double x = 4.0;           

字尾F和M是最有用的,并應該在指定float或decimal字面量時使用。下面的語句不能在沒有字尾F時進行編譯。這是因為4.5會認定為double而double是無法隐式轉換為float的:

float f = 4.5F;           

同樣的規則也适用于decimal字面量:

decimal d = -1.23M;     // Will not compile without the M suffix.           

我們将在下一節詳細介紹數值轉換的語義。

2.4.2 數值轉換

2.4.2.1 整數類型到整數類型的轉換

整數類型轉換在目标類型能夠表示源類型的所有可能值時是隐式轉換,否則需要顯式轉換。例如:

int x = 12345;       // int is a 32-bit integer
long y = x;          // Implicit conversion to 64-bit integral type
short z = (short)x;  // Explicit conversion to 16-bit integral type           

2.4.2.2 浮點類型到浮點類型的轉換

double能表示所有可能的float值,是以float能隐式轉換為double。反之則必須是顯式轉換。

2.4.2.3 浮點類型到整數類型的轉換

所有整數類型可以隐式轉換為浮點數類型:

int i = 1;
float f = i;           

反之則必須是顯式轉換:

int i2 = (int)f;           

将浮點數轉換為整數時,小數點後的數值将被截去而不會舍入。靜态類System.Convert提供了在不同值類型之間轉換的舍入方法(見第6章)。

将大的整數類型隐式轉換為浮點類型會保留數值部分,但是有時會丢失精度。這是因為浮點類型雖然擁有比整數類型更大的數值,但是有時其精度卻比整數類型要小。以下代碼用一個更大的數重複上述示例展示了這種精度丢失的情況:

int i1 = 100000001;
float f = i1;          // Magnitude preserved, precision lost
int i2 = (int)f;       // 100000000           

2.4.2.4 decimal類型轉換

所有的整數類型都能隐式轉換為decimal類型。這是因為decimal可以表示所有可能的C#整數類型值。其他所有的數值類型轉換為decimal或從decimal類型進行轉換都必須是顯式轉換。

2.4.3 算術運算符

算式運算符(+、-、*、/、%)可用于除8位和16位的整數類型之外的所有數值類型:

+    Addition
-    Subtraction
*    Multiplication
/    Division
%    Remainder after division           

2.4.4 自增和自減運算符

自增和自減運算符(++、--)分别給數值類型加1或者減1。具體要将其放在變量之前還是之後則取決于需要得到變量在自增/自減之前的值還是之後的值。例如:

int x = 0, y = 0;
Console.WriteLine (x++);   // Outputs 0; x is now 1
Console.WriteLine (++y);   // Outputs 1; y is now 1           

2.4.5 特殊整數類型運算

(整數類型指int、uint、long、ulong、short、ushort、byte和sbyte。)

2.4.5.1 整數除法

整數類型的除法運算總是會截斷餘數(向0舍入)。用一個值為0的變量做除數将産生運作時錯誤(DivideByZeroException):

int a = 2 / 3;      // 0

int b = 0;
int c = 5 / b;      // throws DivideByZeroException           

用字面量式常量0做除數将産生編譯時錯誤。

2.4.5.2 整數溢出

在運作時執行整數類型的算術運算可能會造成溢出。預設情況下,溢出會默默地發生而不會抛出任何異常,且其溢出行為是“循環”的。就像是運算發生在更大的整數類型上,而超出部分的進位就被丢棄了。例如,減少最小的整數值将産生最大的整數值:

int a = int.MinValue;
a--;
Console.WriteLine (a == int.MaxValue); // True
           

2.4.5.3 整數運算溢出檢查運算符

checked運算符的作用是:在運作時當整數類型表達式或語句超過相應類型的算術限制時不再默默地溢出,而是抛出OverflowException。checked運算符可在有++、--、+、-(一進制運算符和二進制運算符)、*、/和整數類型間顯式轉換運算符的表達式中起作用。

checked運算符對double和float類型沒有作用(它們會溢出為特殊的“無限”值,這會在後面介紹),對decimal類型也沒有作用(這種類型總是會進行溢出檢查)。

checked運算符能和表達式或語句塊結合使用,例如:

int a = 1000000;
int b = 1000000;

int c = checked (a * b);      // Checks just the expression.

checked                       // Checks all expressions
{                             // in statement block.
   ...
   c = a * b;
   ...
}
           

可以在編譯時加上/checked+指令行開關(在Visual Studio中,可以在“Advanced Build Settings”中設定)來預設使程式中所有表達式都進行算術溢出檢查。如果你隻想禁用指定表達式或語句的溢出檢查,可以用unchecked運算符。例如,下面的代碼即使在編譯時使用了/checked+也不會抛出異常:

int x = int.MaxValue;
int y = unchecked (x + 1);
unchecked { int z = x + 1; }           

2.4.5.4 常量表達式的溢出檢查

無論是否使用了/checked編譯器開關,編譯時的表達式計算總會檢查溢出,除非應用了unchecked運算符。

int x = int.MaxValue + 1;               // Compile-time error
int y = unchecked (int.MaxValue + 1);   // No errors           

2.4.5.5 位運算符

C#支援以下的位運算符:

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

2.4.6 8位和16位整數類型

8位和16位整數類型指byte、sbyte、short、ushort。這些類型自己并不具備算術運算符,是以C#隐式地将它們轉換為所需的更大一些的類型。當試圖把運算結果賦給一個小的整數類型時會産生編譯時錯誤:

short x = 1, y = 1;
short z = x + y;          // Compile-time error           

在以上情況下,x和y會隐式轉換成int以便進行加法運算。是以運算結果也是int,它不能隐式轉換回short(因為這可能會造成資料丢失)。我們必須使用顯式轉換才能令其通過編譯:

short z = (short) (x + y);   // OK           

2.4.7 特殊的float和double值

不同于整數類型,浮點類型包含某些特定運算需要特殊對待的值。這些特殊的值是NaN(Not a Number,非數字)、+∞、-∞和-0。float和double類型包含表示NaN、+∞、-∞值的常量。其他的常量還有MaxValue、MinValue以及Epsilon。例如:

Console.WriteLine (double.NegativeInfinity);   // -Infinity           

double和float類型的特殊值的常量表如下:

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

非零值除以零的結果是無窮大。例如:

Console.WriteLine ( 1.0 / 0.0);                  //  Infinity
Console.WriteLine (-1.0 / 0.0);                  // -Infinity
Console.WriteLine ( 1.0 / -0.0);                  // -Infinity
Console.WriteLine (-1.0 / -0.0);                  //  Infinity           

零除以零或無窮大減去無窮大的結果是NaN。例如:

Console.WriteLine ( 0.0 /  0.0);                 //  NaN
Console.WriteLine ((1.0 /  0.0) - (1.0 / 0.0));   //  NaN
           

使用比較運算符(==)時,一個NaN的值永遠也不等于其他的值,甚至不等于其他的NaN值:

Console.WriteLine (0.0 / 0.0 == double.NaN);    // False           

必須使用float.IsNaN或double.IsNaN方法來判斷一個值是否為NaN:

Console.WriteLine (double.IsNaN (0.0 / 0.0));   // True           

但使用object.Equals方法時,兩個NaN卻是相等的:

Console.WriteLine (object.Equals (0.0 / 0.0, double.NaN));   // True           

NaN在表示特殊值時很有用。在WPF中,double.NaN表示值為“Automatic”(自動)。另一種表示方法是使用可空類型(nullable,見第4章)。還可以使用一個包含數值類型和一個額外字段的自定義結構體(見第3章)。

float和double遵循IEEE 754格式類型規範。幾乎所有的處理器都原生支援此規範。如需此類型行為的詳細資訊,可參考

http://www.ieee.org

2.4.8 double和decimal的對比

double類型在科學計算(例如計算空間坐标)時很有用。decimal類型在金融計算和計算那些“人為”的而非真實世界度量的結果時很有用。下面是這兩種類型的不同之處:

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

2.4.9 實數的舍入誤差

float和double在内部都是基于2來表示數值的。是以隻有基于2表示的數值才能夠精确表示。事實上,這意味着大多數有小數部分的字面量(它們都基于10)将無法精确表示。例如:

float tenth = 0.1f;                       // Not quite 0.1
float one   = 1f;
Console.WriteLine (one - tenth * 10f);    // -1.490116E-08
           

這就是為什麼float和double不适合金融運算。相反,decimal基于10,它能夠精确表示基于10的數值(也包括它的因數,基于2和基于5的數值)。因為實數的字面量都是基于10的,是以decimal能夠精确表示像0.1這樣的數。然而,double和decimal都不能精确表示那些基于10的循環小數:

decimal m = 1M / 6M;               // 0.1666666666666666666666666667M
double  d = 1.0 / 6.0;             // 0.16666666666666666
           

這将會導緻積累性的舍入誤差:

decimal notQuiteWholeM = m+m+m+m+m+m;  // 1.0000000000000000000000000002M
double  notQuiteWholeD = d+d+d+d+d+d;  // 0.99999999999999989
           

這也将影響相等和比較操作:

Console.WriteLine (notQuiteWholeM == 1M);   // False
Console.WriteLine (notQuiteWholeD < 1.0);   // True
           

2.5 布爾類型和運算符

C#中的bool(System.Boolean類型的别名)類型是能指派為true和false字面量的邏輯值。

盡管布爾類型的值僅需要1位的存儲空間,但是運作時卻使用了1位元組記憶體空間。這是因為位元組是運作時和處理器能夠有效使用的最小機關。為避免在使用數組時的空間浪費,.NET Framework在System.Collections指令空間下提供了BitArray類,其中的每一個布爾值僅占用一位。

2.5.1 布爾類型轉換

bool類型不能轉換為數值類型,反之亦然。

2.5.2 相等和比較運算符

==和!=用于判斷任意類型的相等與不等,并總是傳回一個bool值。注3值類型通常有很簡單的相等定義:

int x = 1;
int y = 2;
int z = 1;
Console.WriteLine (x == y);         // False
Console.WriteLine (x == z);         // True
           

對于引用類型,預設情況下相等是基于引用的,而不是底層對象的實際值(更多内容請參見第6章):

public class Dude
{
  public string Name;
  public Dude (string n) { Name = n; }
}
...
Dude d1 = new Dude ("John");
Dude d2 = new Dude ("John");
Console.WriteLine (d1 == d2);       // False
Dude d3 = d1;
Console.WriteLine (d1 == d3);       // True
           

相等和比較運算符==、!=、<、>、>=和<=可用于所有的數值類型,但是用于實數時要特别注意(請參見2.4.9節)。比較運算符也可以用于枚舉(enum)類型的成員,它比較的是表示枚舉成員的整數值,我們将在3.7節中介紹。

我們将在4.14節、6.11節和6.12節中詳細介紹相等和比較運算符。

2.5.3 條件運算符

&&和||運算符用于判斷與和或條件。它們常常與代表“非”的!運算符一起使用。在下面的例子中,UseUmbrella方法在下雨或陽光充足(雨傘可以保護我們不會經受日曬雨淋),以及無風(因為雨傘在有風的時候不起作用)的時候傳回true:

static bool UseUmbrella (bool rainy, bool sunny, bool windy)
{
  return !windy && (rainy || sunny);
}
           

&&和||運算符會在可能的情況下執行短路計算。在上面的例子中。如果刮風,(rainy || sunny)将不會計算。短路計算在某些表達式中是非常必要的,它可以允許如下表達式運作而不會抛出NullReferenceException異常:

if (sb != null && sb.Length > 0) ...
           

&和|運算符也可用于判斷與和或條件:

return !windy & (rainy | sunny);
           

不同之處是&和|運算符不支援短路計算。是以它們很少用于替代條件運算符。

不同于C和C++,&和|運算符在用于布爾表達式時執行布爾比較(非短路計算)。而&和|運算符僅在用于數值運算時才執行位運算。

(三元)條件運算符

三元條件運算符(由于它是唯一一個使用三個操作數的運算符,是以也簡稱為三元運算符)使用q ? a : b的形式。它在q為真時計算a否則計算b。例如:

static int Max (int a, int b)
{
  return (a > b) ? a : b;
}
           

條件運算符在LINQ語句中尤其有用(見第8章)。

2.6 字元串和字元

C#的char(System.Char類型的别名)類型表示一個Unicode字元并占用兩個位元組。char字面量應位于兩個單引号之間:

char c = 'A'; // Simple character

轉義字元指那些不能用字面量表示或解釋的字元。轉義字元由反斜線和一個表示特殊含義的字元組成,例如:

char newLine = 'n';

char backSlash = '\';

表2-2中列出了轉義字元序列。

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章
帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

u(或x)轉義字元通過4位十六進制代碼來指定任意Unicode字元:

char copyrightSymbol = '\u00A9';
char omegaSymbol     = '\u03A9';
char newLine         = '\u000A';
           

2.6.1 char轉換

從char類型到數值類型的隐式轉換隻在這個數值類型可以容納無符号short類型時有效。對于其他的數值類型,則需要顯式轉換。

2.6.2 字元串類型

C#中的字元串類型(System.String類型的别名,我們将在第6章詳細介紹)表示不可變的Unicode字元序列。字元串字面量應位于兩個雙引号(")之間:

string a = "Heat";
           

string類型是引用類型而不是值類型。但是它的相等運算符卻遵守值類型的語義。

string a = "test";
string b = "test";
Console.Write (a == b);  // True
           

對char字面量有效的轉義字元在字元串中同樣有效:

string a = "Here's a tab:\t";
           

這意味着當需要一個反斜杠時,需要寫兩次才可以:

string a1 = "\\\\server\\fileshare\\helloworld.cs";
           

為避免這種情況,C#引入了原意字元串字面量。原意字元串字面量要加@字首,它不支援轉義字元。下面的原意字元串和之前的字元串是一樣的。

string a2 = @"\\server\fileshare\helloworld.cs";
           

原意字元串可以貫穿多行:

string escaped  = "First Line\r\nSecond Line";
string verbatim = @"First Line
Second Line";

// True if your IDE uses CR-LF line separators:
Console.WriteLine (escaped == verbatim);
           

原意字元串中需要用兩個雙引号來表示一個雙引号字元:

string xml = @"<customer id=""123""></customer>";
           

2.6.2.1 連接配接字元串

+運算符可連接配接兩個字元串:

string s = "a" + "b";
           

如果操作數之一是非字元串值,則會調用其ToString方法,例如:

string s = "a" + 5;  // a5
           

重複使用+運算符來建構字元串是低效的。更好的解決方案是使用System.Text.StringBuilder類型(将在第6章介紹)。

2.6.2.2 字元串插值(C# 6)

以$字元為字首的字元串稱為插值字元串。插值字元串可以在大括号内包含表達式:

int x = 4;
Console.Write ($"A square has {x} sides");  // Prints: A square has 4 sides
           

大括号内可以是任意類型的合法C#表達式。C#會調用其ToString方法或等價方法将表達式轉換為字元串。若要更改表達式的格式,可以使用冒号,并繼以格式字元串(我們将在6.1.2.7節中對其詳細介紹):

string s = $"255 in hex is {byte.MaxValue:X2}";  // X2 = 2-digit Hexadecimal
// Evaluates to "25 in hex is FF"
           

插值字元串隻能是在單行内聲明,除非使用原意字元串運算符。需要注意,$運算符必須在@運算符之前:

int x = 2;
string s = $@"this spans {
x} lines";
           

若要在插值字元串中表示大括号字元隻需書寫兩個大括号字元即可。

2.6.2.3 字元串比較

string類型不支援<和>的比較。必須使用字元串的CompareTo方法。這部分内容将在第6章介紹。

2.7 數組

數組是固定數量的特定類型的變量集合(稱為元素)。為了實作高效通路,數組中的元素總是存儲在連續的記憶體塊中。

C#中的數組用元素類型後加方括号的方式表示。例如:

char[] vowels = new char[5];    // Declare an array of 5 characters
           

方括号也可用于檢索數組,通過位置通路特定元素:

vowels[0] = 'a';
vowels[1] = 'e';
vowels[2] = 'i';
vowels[3] = 'o';
vowels[4] = 'u';
Console.WriteLine (vowels[1]);      // e
           

因為數組索引從0開始,是以上面的語句列印“e”。我們可以使用for循環語句來周遊數組中的每一個元素。下面例子中的for循環将把整數變量i從0到4進行循環:

for (int i = 0; i < vowels.Length; i++)
  Console.Write (vowels[i]);            // aeiou
           

數組的Length屬性傳回數組中的元素數目。一旦數組建立完畢,它的長度将不能更改。System.Collection命名空間和子命名空間提供了可變長度數組和字典等進階資料結構。

數組初始化表達式可以讓你一次性聲明并填充數組:

char[] vowels = new char[] {'a','e','i','o','u'};
           

或者簡寫為:

char[] vowels = {'a','e','i','o','u'};
           

所有的數組都繼承自System.Array類。它為所有數組提供了通用服務。這些成員包括與數組類型無關的擷取和設定數組元素的方法,我們将在第7章介紹。

2.7.1 預設數組元素初始化

建立數組時其元素總會用預設值初始化。類型的預設值是按位取0的記憶體表示的值。例如,若定義一個整數數組,由于int是值類型,是以該操作會在連續的記憶體塊中配置設定1000個整數。每一個元素的預設值都是0:

int[] a = new int[1000];
Console.Write (a[123]);            // 0
           

值類型和引用類型的差別

數組元素的類型是值類型還是引用類型對其性能有重要的影響。若元素類型是值類型,每個元素的值将作為數組的一部分進行配置設定,例如:

public struct Point { public int X, Y; }
...

Point[] a = new Point[1000];
int x = a[500].X;                  // 0
           

若Point是類,建立數組則僅僅配置設定了1000個空引用:

public class Point { public int X, Y; }

...
Point[] a = new Point[1000];
int x = a[500].X;            // Runtime error, NullReferenceException
           

為避免這個錯誤,我們必須在執行個體化數組之後顯式執行個體化1000個Point執行個體:

Point[] a = new Point[1000];
for (int i = 0; i < a.Length; i++) // Iterate i from 0 to 999
   a[i] = new Point();             // Set array element i with new point
           

不論元素是何種類型,數組本身總是引用類型對象。例如,下面的語句是合法的:

int[] a = null;
           

2.7.2 多元數組

多元數組分為兩種類型:矩形數組和鋸齒形數組。矩形數組代表n維的記憶體塊,而鋸齒形數組則是數組的數組。

2.7.2.1 矩形數組

矩形數組聲明時用逗号分隔每個次元。下面的語句聲明了一個矩形二維數組,它的次元是3×3:

int[,] matrix = new int[3,3];
           

數組的GetLength方法傳回給定次元的長度(從0開始):

for (int i = 0; i < matrix.GetLength(0); i++)
  for (int j = 0; j < matrix.GetLength(1); j++)
    matrix[i,j] = i * 3 + j;
           

矩形數組可以按照如下方式進行初始化(以下示例建立了一個和上例一樣的數組):

int[,] matrix = new int[,]
{
  {0,1,2},
  {3,4,5},
  {6,7,8}
};
           

2.7.2.2 鋸齒形數組

鋸齒形數組在聲明時用一對方括号對表示每個次元。以下例子聲明了一個最外層次元是3的二維鋸齒形數組:

int[][] matrix = new int[3][];
           

有意思的是,這裡是new int[3][]而非new int[][3]。Eric Lippert有一篇文章詳細解釋了這個問題,請參見:

http://albahari.com/jagged

不同于矩形數組,鋸齒形數組内層次元在聲明時并未指定。每個内層數組都可以是任意長度。每一個内層數組都隐式初始化為null而不是一個空數組,是以都需要手動建立:

for (int i = 0; i < matrix.Length; i++)
{
  matrix[i] = new int[3];                    // Create inner array
  for (int j = 0; j < matrix[i].Length; j++)
    matrix[i][j] = i * 3 + j;
}
           

鋸齒形數組可以按照如下方式進行初始化(以下例子建立了一個和前面例子類似的數組,但是在最後額外追加了一個元素):

int[][] matrix = new int[][]
{
  new int[] {0,1,2},
  new int[] {3,4,5},
  new int[] {6,7,8,9}
};
           

2.7.3 簡化數組初始化表達式

有兩種方式可以簡化數組初始化表達式。第一種是省略new運算符和類型限制條件:

char[] vowels = {'a','e','i','o','u'};

int[,] rectangularMatrix =
{
  {0,1,2},
  {3,4,5},
  {6,7,8}
};

int[][] jaggedMatrix =
{
  new int[] {0,1,2},
  new int[] {3,4,5},
  new int[] {6,7,8}
};
           

第二種是使用var關鍵字,使編譯器隐式确定局部變量類型:

var i = 3;           // i is implicitly of type int
var s = "sausage";   // s is implicitly of type string

// Therefore:

var rectMatrix = new int[,]    // rectMatrix is implicitly of type int[,]
{
  {0,1,2},
  {3,4,5},
  {6,7,8}
};

var jaggedMat = new int[][]    // jaggedMat is implicitly of type int[][]
{
  new int[] {0,1,2},
  new int[] {3,4,5},
  new int[] {6,7,8}
};           

數組類型可以進一步應用隐式類型轉換規則:可以直接在new關鍵字之後忽略類型限定符,而由編譯器推斷數組類型:

var vowels = new[] {'a','e','i','o','u'};   // Compiler infers char[]
           

為了使上述機制工作,數組中的所有元素必須能夠隐式轉換為一種類型(至少有一個元素是目标類型,而且最終隻有一種最佳類型),例如:

var x = new[] {1,10000000000};   // all convertible to long
           

2.7.4 邊界檢查

運作時會為所有數組的索引操作進行邊界檢查。如果是用了不合法的索引值,就會抛出IndexOutOfRangeException異常。

int[] arr = new int[3];
arr[3] = 1;               // IndexOutOfRangeException thrown
           

與Java一樣,數組邊界檢查對類型安全和調試簡化都是非常必要的。

通常,邊界檢查的性能開銷很小,且JIT(即時編譯器)也會對其進行優化。例如,在進入循環之前預先確定所有的索引操作的安全性來避免每次循環中都進行檢查。另外C#還提供了unsafe代碼來顯式繞過邊界檢查(請參見4.15節)。

2.8 變量和參數

變量表示存儲着可變值的存儲位置。變量可以是局部變量、參數(value、ref或out),字段(執行個體或靜态)以及數組元素。

2.8.1 棧和堆

棧和堆是存儲變量和常量的地方。它們分别具有不同的生命周期語義。

2.8.1.1 棧

棧是存儲局部變量和參數的記憶體塊。邏輯上,棧會在函數進入和退出時增加或減少。考慮下面的方法(為了避免幹擾,該範例省略了輸入參數檢查):

static int Factorial (int x)
{
  if (x == 0) return 1;
  return x * Factorial (x-1);
}
           

這個方法是遞歸的,即它調用其自身。每一次進入這個方法的時候,就在棧上配置設定一個新的int,而每一次離開這個方法,就會釋放一個int。

2.8.1.2 堆

堆是儲存對象(例如引用類型的執行個體)的記憶體塊。新建立的對象會配置設定在堆上并傳回其引用。程式執行過程中,堆就被新建立的對象不斷填充。.NET運作時的垃圾回收器會定期從堆上釋放對象,是以應用程式不會記憶體不足。隻要對象沒有被“存活”的對象引用,它就可以被釋放。

下面的例子中,我們建立了一個StringBuilder對象并将其引用指派給ref1變量,之後在其中寫入内容。StringBuilder對象在後續沒有使用的情況下可立即被垃圾回收器釋放。

之後,我們建立另一個StringBuilder對象指派給ref2,再将引用複制給ref3。雖然ref2之後便不再使用,但是由于ref3保持着同一個StringBuilder對象的引用,是以在ref3使用完畢之前它不會被垃圾回收器回收。

using System;
using System.Text;

class Test
{
  static void Main()
  {
    StringBuilder ref1 = new StringBuilder ("object1");
    Console.WriteLine (ref1);
    // The StringBuilder referenced by ref1 is now eligible for GC.

    StringBuilder ref2 = new StringBuilder ("object2");
    StringBuilder ref3 = ref2;
    // The StringBuilder referenced by ref2 is NOT yet eligible for GC.

    Console.WriteLine (ref3);                   // object2
  }
}           

值類型的執行個體(和對象的引用)就存儲在變量聲明的地方。如果聲明為類的字段或數組的元素,則該執行個體會存儲在堆上。

C#中無法像C++那樣顯式删除對象。未引用的對象最終将被垃圾回收器回收。

靜态字段也會存儲在堆上。與配置設定在堆上的對象(可以被垃圾回收)不同,這些變量一直存活直至應用程式域結束。

2.8.2 明确指派

C#強制執行明确指派政策。實踐中這意味着在unsafe上下文之外無法通路未初始化的記憶體。明确指派有三種含義:

局部變量在讀取之前必須指派。

調用方法時必須提供函數的實際參數(除非标記為可選參數,參見2.8.4.7節)。

運作時将自動初始化其他變量(例如字段和數組元素)。

例如,以下示例将産生編譯時錯誤:

static void Main()
{
  int x;
  Console.WriteLine (x);        // Compile-time error
}
           

字段和數組元素會自動初始化為其類型的預設值。以下代碼輸出0,就是因為數組元素會隐式賦為預設值:

static void Main()
{
  int[] ints = new int[2];
  Console.WriteLine (ints[0]);    // 0
}
           

以下代碼輸出0,因為字段會隐式指派為預設值:

class Test
{
  static int x;
  static void Main() { Console.WriteLine (x); }   // 0
}
           

2.8.3 預設值

所有類型的執行個體都有預設值。預定義類型的預設值是按位取0的記憶體表示的值。

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

default關鍵字可用于獲得任意類型的預設值(這對泛型非常有用,我們将在第3章介紹泛型)。

decimal d = default (decimal);

自定義值類型(例如struct)的預設值等同于每一個字段都取其預設值。

2.8.4 參數

方法可以有一連串的參數(parameter)。在調用方法時必須為這些參數提供實際值(argument)。在下面的例子中,Foo方法僅有一個類型為int的參數p:

static void Foo (int p)
{
  p = p + 1;                 // Increment p by 1
  Console.WriteLine (p);     // Write p to screen
}

static void Main()
{
  Foo (8);                  // Call Foo with an argument of 8
}
           

使用ref和out修飾符可以控制參數的傳遞方式:

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

2.8.4.1 按值傳遞參數

預設情況下,C#中的參數預設按值傳遞,這是最常用的方式。這意味着在将參數值傳遞給方法時将建立一份參數值的副本:

class Test
{
  static void Foo (int p)
  {
    p = p + 1;                // Increment p by 1
    Console.WriteLine (p);    // Write p to screen
  }

  static void Main()
  {
    int x = 8;
    Foo (x);                  // Make a copy of x
    Console.WriteLine (x);    // x will still be 8
  }
}
           

為p賦一個新的值并不會改變x的值,因為p和x分别存儲在不同的記憶體位置中。

按值傳遞引用類型參數複制的是引用而非對象本身。下例中,Foo方法中的StringBuilder對象和Main方法中執行個體化的是同一個對象,但是它們的引用是不同的。換句話說,變量sb和fooSB是引用同一個StringBuilder對象的不同變量:

class Test
{
  static void Foo (StringBuilder fooSB)
  {
    fooSB.Append ("test");
    fooSB = null;
  }

  static void Main()
  {
    StringBuilder sb = new StringBuilder();
    Foo (sb);
    Console.WriteLine (sb.ToString());    // test
  }
}
           

由于fooSB是引用的一份副本,是以将它指派為null并不會把sb也指派為null(然而,如果在聲明和調用fooSB時使用ref修飾符,則sb會變成null)。

2.8.4.2 ref修飾符

在C#中,若按引用傳遞參數則應使用ref參數修飾符。下面的例子中,p和x指向同一塊記憶體位置:

class Test
{
  static void Foo (ref int p)
  {
    p = p + 1;               // Increment p by 1
    Console.WriteLine (p);   // Write p to screen
  }

  static void Main()
  {
    int x = 8;
    Foo (ref  x);            // Ask Foo to deal directly with x
    Console.WriteLine (x);   // x is now 9
  }
}
           

現在給p賦新值将改變x的值。注意ref修飾符在聲明和調用時都是必須的,注4這樣就清楚地表明了程式将如何執行。

ref修飾符對于實作交換方法是必要的。(3.9節将介紹如何編寫适用于所有類型的交換方法):

class Test
{
  static void Swap (ref string a, ref string b)
  {
    string temp = a;
    a = b;
    b = temp;
  }

  static void Main()
  {
    string x = "Penn";
    string y = "Teller";
    Swap (ref x, ref y);
    Console.WriteLine (x);   // Teller
    Console.WriteLine (y);   // Penn
  }
}
           

無論參數是引用類型還是值類型,都可以按引用傳遞或按值傳遞。

2.8.4.3 out修飾符

out參數和ref參數類似,但在以下幾點上不同:

不需要在傳入函數之前進行指派。

必須在函數結束之前指派。

out修飾符通常用于獲得方法的多個傳回值,例如:

class Test
{
  static void Split (string name, out string firstNames,
                     out string lastName)
  {
     int i = name.LastIndexOf (' ');
     firstNames = name.Substring (0, i);
     lastName   = name.Substring (i + 1);
  }

  static void Main()
  {
    string a, b;
    Split ("Stevie Ray Vaughan", out a, out b);
    Console.WriteLine (a);                      // Stevie Ray
    Console.WriteLine (b);                      // Vaughan
  }
}           

與ref參數一樣,out參數按引用傳遞。

2.8.4.4 out變量及丢棄變量(C# 7)

從C# 7開始,允許在調用含有out參數的方法時直接聲明變量。是以我們可以将前面例子中的Main方法簡化為:

static void Main()
{
  Split ("Stevie Ray Vaughan", out string a, out string b);
  Console.WriteLine (a);                      // Stevie Ray
  Console.WriteLine (b);                      // Vaughan
}
           

當調用含有多個out參數的方法時,若我們并非關注所有參數的值,那麼可以使用下劃線來“丢棄”那些不感興趣的參數:

Split ("Stevie Ray Vaughan", out string a, out _);   // Discard the 2nd param
Console.WriteLine (a);
           

此時,編譯器會将下劃線認定為一個特殊的符号,稱為丢棄符号。一次調用可以引入多個丢棄符号。假設SomeBigMethod定義了7個out參數,除第4個之外其他的全部被丢棄:

SomeBigMethod (out _, out _, out _, out int x, out _, out _, out _);
           

出于向後相容性的考慮,如果在作用域内,已經有一個名為下劃線的變量的話,這個語言特性就失效了。

string _;
Split ("Stevie Ray Vaughan", out string a, _);   // Will not compile
           

2.8.4.5 按引用傳遞的含義

按引用傳遞參數是為現存變量的存儲位置起了一個别名而不是建立一個新的存儲位置。下面的例子中,變量x和y代表相同的執行個體:

class Test
{
  static int x;

  static void Main() { Foo (out x); }

  static void Foo (out int y)
  {
    Console.WriteLine (x);                // x is 0
    y = 1;                                // Mutate y
    Console.WriteLine (x);                // x is 1
  }
}
           

2.8.4.6 params修飾符

params參數修飾符隻能修飾方法中的最後一個參數,它能夠使方法接受任意數量的指定類型參數。參數類型必須聲明為數組,例如:

class Test
{
  static int Sum (params int[] ints)
  {
    int sum = 0;
    for (int i = 0; i < ints.Length; i++)
      sum += ints[i];                       // Increase sum by ints[i]
    return sum;
  }

  static void Main()
  {
    int total = Sum (1, 2, 3, 4);
    Console.WriteLine (total);              // 10
  }
}
           

也可以将普通的數組提供給params參數。是以Main方法的第一行從語義上等價于:

int total = Sum (new int[] { 1, 2, 3, 4 } );
           

2.8.4.7 可選參數

從C# 4.0開始,方法、構造器和索引器(見第3章)中都可以聲明可選參數。隻要在參數聲明中提供預設值,這個參數就是可選參數:

void Foo (int x = 23) { Console.WriteLine (x); }
           

可選參數在調用方法時可以省略:

Foo();     // 23
           

預設參數23實際上傳遞給了可選參數x,編譯器在調用端将值23傳遞到編譯好的代碼中。上例中調用Foo的代碼語義上等價于:

Foo (23);
           

這是由于編譯器在用到可選參數的地方使用預設值代替可選參數而造成的結果。

若public方法對其他程式集可見,則在添加可選參數時雙方均需重新編譯,就像參數是必須提供的一樣。

可選參數的預設值必須由常量表達式或者無參數的值類型構造器指定,可選參數不能标記為ref或者out。

必填參數必須在可選參數方法聲明和調用之前出現(params參數例外,它總是最後出現)。下面的例子将1顯式傳遞給參數x,而将預設值0傳遞給參數y:

void Foo (int x = 0, int y = 0) { Console.WriteLine (x + ", " + y); }

void Test()
{
  Foo(1);    // 1, 0
}
           

相反,如需傳遞預設值給x而傳遞顯式值給y,則必須聯合使用命名參數與可選參數。

2.8.4.8 命名參數

除了用位置确定參數外,還可以用名稱來确定參數,例如:

void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }

void Test()
{
  Foo (x:1, y:2);  // 1, 2
}
           

命名參數能夠以任意順序出現。下面兩種調用Foo的方式在語義上是一樣的:

Foo (x:1, y:2);
Foo (y:2, x:1);
           

上述寫法的不同之處的是參數表達式将按調用端參數出現的順序計算。通常,這種不同隻出現在非獨立的擁有副作用的表達式中。例如下面的代碼将輸出0,1:

int a = 0;
Foo (y: ++a, x: --a);  // ++a is evaluated first
           

當然,在實踐中應當避免這種代碼。

命名參數和可選參數可以混合使用:

Foo (1, y:2);
           

然而這裡有一個限制,按位置傳遞的參數必須出現在命名參數之前。是以不能這樣調用Foo方法:

Foo (x:1, 2);         // Compile-time error
           

命名參數在和可選參數混合使用時特别有效。例如,考慮下面的方法:

void Bar (int a = 0, int b = 0, int c = 0, int d = 0) { ... }
           

我們可以用以下方式在調用它的時候僅提供d的值:

Bar (d:3);
           

這個特性在調用COM API時非常有用。我們将在5.2.17節詳細讨論。

2.8.5 引用局部變量(C# 7)

C# 7添加了一個令人費解的特性:即定義一個用于引用數組中某一個元素或對象中某一個字段的局部變量:

int[] numbers = { 0, 1, 2, 3, 4 };
ref int numRef = ref numbers [2];
           

在這個例子中,numRef是numbers[2]的引用。當我們更改numRef的值時,也相應更改了數組中的元素值:

numRef *= 10;
Console.WriteLine (numRef);        // 20
Console.WriteLine (numbers [2]);   // 20
           

引用局部變量的目标隻能是數組的元素、對象字段或者局部變量;而不能是屬性(見第3章)。引用局部變量适用于在特定的場景下進行小範圍優化,并通常和引用傳回值合并使用。

2.8.6 引用傳回值(C# 7)

從方法中傳回的引用局部變量,稱為引用傳回值

(ref return):
static string X = "Old Value";

static ref string GetX() => ref X;    // This method returns a ref

static void Main()
{
  ref string xRef = ref GetX();       // Assign result to a ref local
  xRef = "New Value";
  Console.WriteLine (X);              // New Value
}
           

2.8.7 var隐式類型局部變量

我們通常會在一步中完成變量的聲明和初始化。如果編譯器能夠從初始化表達式中推斷出變量的類型,就能夠使用var關鍵字(C# 3.0引入)來代替類型聲明,例如:

var x = "hello";
var y = new System.Text.StringBuilder();
var z = (float)Math.PI;
           

它們完全等價于:

string x = "hello";
System.Text.StringBuilder y = new System.Text.StringBuilder();
float z = (float)Math.PI;
           

因為是完全等價的,是以隐式類型變量仍是靜态類型的。例如,下面的代碼将産生編譯時錯誤:

var x = 5;
x = "hello";    // Compile-time error; x is of type int
           

當無法直接從變量聲明語句中看出變量類型的時候,var關鍵字将降低代碼的可讀性。例如:

Random r = new Random();
var x = r.Next();
           

變量x的類型是什麼呢?

在4.9節我們将介紹必須使用var的情況。

2.9 表達式和運算符

表達式本質上是值。最簡單的表達式是常量和變量。表達式能夠用運算符進行轉換群組合。運算符用一個或多個輸入操作數來輸出一個新的表達式。

以下是一個常量表達式的例子:

12
           

可以使用*運算符來組合兩個操作數(字面量表達式12和30):

12 * 30
           

由于操作數本身可以是表達式,是以可以創造出更複雜的表達式。例如,(12 * 30)是下面的表達式中的操作數。

1 + (12 * 30)
           

C#中的運算符分為一進制運算符、二進制運算符和三元運算符,這取決于它們使用的操作數數量(1、2或3)。二進制運算符總是使用中綴表示法,運算符在兩個操作數之間。

2.9.1 基礎表達式

基礎表達式由C#語言内置的基礎運算符表達式組成,例如:

Math.Log (1)
           

這個表達式由兩個基礎表達式構成,第一個表達式執行成員查找(用.運算符),而第二個表達式執行方法調用(用()運算符)。

2.9.2 空表達式

空表達式(void expression)是沒有值的表達式,例如:

Console.WriteLine (1)
           

因為空表達式沒有值,是以不能作為操作數來建立更複雜的表達式:

1 + Console.WriteLine (1)      // Compile-time error
           

2.9.3 指派表達式

指派表達式用=運算符将另一個表達式的值指派給變量,例如:

x = x * 5
           

指派表達式不是一個空表達式,它的值即是被賦予的值。是以指派表達式可以和其他表達式組合。下面的例子中,表達式将2賦給x并将10賦給y:

y = 5 * (x = 2)
           

這種類型的表達式也可以用于初始化多個值:

a = b = c = d = 0
           

複合指派運算符是由其他運算符組合而成的簡化運算符。例如:

x *= 2    // equivalent to x = x * 2
x <<= 1   // equivalent to x = x << 1
           

(這條規則的例外是第4章中介紹的事件(event)。它的+=和-=運算符會特殊對待并映射至事件的add和remove通路器上。)

2.9.4 運算符優先級和結合性

當表達式包含多個運算符時,運算符的優先級和結合性決定了計算的順序。優先級高的運算符先于優先級低的運算符執行。如果運算符的優先級相同,那麼運算符的結合性決定計算的順序。

2.9.4.1優先級

以下的表達式:

1 + 2 * 3
           

由于*的優先級高于+,是以它将按下面的方式計算:

1 + (2 * 3)
           

2.9.4.2 左結合運算符

二進制運算符(除了指派運算符、Lambda運算符、null合并運算符)是左結合運算符。換句話說,它們是從左往右計算。例如,下面的表達式:

8 / 4 / 2

由于左結合性将按如下的方式計算:

( 8 / 4 ) / 2 // 1

插入括号可以改變實際的計算順序:

8 / ( 4 / 2 ) // 4

2.9.4.3 右結合運算符

指派運算符、Lambda運算符、null合并運算符和條件運算符是右結合的。換句話說,它們是從右往左計算。右結合性允許多重指派,例如:

x = y = 3;

首先将3指派給y,之後再将表達式(3)的結果指派給x。

2.9.5 運算符表

表2-3按照優先級列出了C#的運算符。同一類别的運算符的優先級相同。我們将在4.14節介紹使用者可重載的運算符。

帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章
帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章
帶你讀《C# 7.0核心技術指南》之二:C#語言基礎第2章

2.10 null運算符

C#提供了兩個簡化null處理的運算符:null合并運算符和null條件運算符。

2.10.1 null合并運算符

null合并運算符寫作??。它的意思是“如果操作數不是null則結果為操作數,否則結果為一個預設的值。”例如:

string s1 = null;

string s2 = s1 ?? "nothing"; // s2 evaluates to "nothing"

如果左側的表達式不是null,則右側的表達式将不會進行計算。null合并運算符同樣适用于可空的值類型(請參見4.7節)。

2.10.2 null條件運算符(C# 6)

C# 6中引入了“?.”運算符,稱為null條件運算符或者Elvis運算符(從Elvis表情符号而來)。該運算符可以像标準的“.”運算符那樣通路成員以及調用方法。當運算符的左側為null的時候,該表達式的運算結果也是null而不會抛出NullReferenceException異常。

System.Text.StringBuilder sb = null;
string s = sb?.ToString();  // No error; s instead evaluates to null
           

上述代碼的最後一行等價于:

string s = (sb == null ? null : sb.ToString());
           

當遇到null時,Elvis運算符将直接略過表達式的其餘部分。在接下來的例子中,即使ToString()和ToUpper()方法使用的是标準的.運算符,s的值仍然為null。

System.Text.StringBuilder sb = null;
string s = sb?.ToString().ToUpper();   // s evaluates to null without error
           

僅當直接的左側運算數有可能為null的時候才有必要重複使用Elvis運算符。是以下述表達式在x和y都為null時依然是健壯的:

x?.y?.z
           

它等價于(唯一的不同在于x.y僅執行了一次):

x == null ? null
          : (x.y == null ? null : x.y.z)
           

需要指出,最終的表達式必須能夠處理null,是以下面的範例是不合法的:

System.Text.StringBuilder sb = null;
int length = sb?.ToString().Length;   // Illegal : int cannot be null
           

我們可以使用可空類型(請參見4.7節)來修正這個問題。例如:

int? length = sb?.ToString().Length;   // OK : int? can be null
           

我們也可以使用null條件運算符調用傳回值為void的方法:

someObject?.SomeVoidMethod();

如果someObject為null,則表達式将“不執行指令”而不會抛出NullReference-Exception異常。

null條件運算符可以和第3章介紹的常用類型成員一起使用,包括方法、字段、屬性和索引器。而且它也可以和null合并運算符配合使用。

System.Text.StringBuilder sb = null;
string s = sb?.ToString() ?? "nothing";   // s evaluates to "nothing"
           

2.11 語句

函數是語句構成的。語句按照出現的字面順序執行。語句塊則是在大括号({})中的一系列語句。

2.11.1 聲明語句

聲明語句可以聲明新的變量,并可以用表達式初始化變量。聲明語句以分号結束。可以用逗号分隔的清單聲明多個同類型的變量。例如:

string someWord = "rosebud";

int someNumber = 42;

bool rich = true, famous = false;

常量的聲明和變量類似,但是它的值無法在聲明之後改變,并且變量初始化必須和聲明同時進行(請參見3.1.8節):

const double c = 2.99792458E08;

c += 10; // Compile-time Error

局部變量

局部變量和常量的作用範圍在目前的語句塊中。在目前語句塊或者嵌套的語句塊中聲明另一個同名的局部變量是不行的,例如:

static void Main()
{
  int x;
  {
    int y;
    int x;            // Error - x already defined
  }
  {
    int y;            // OK - y not in scope
  }
  Console.Write (y);  // Error - y is out of scope
}
           

變量的作用範圍是它所在的整個代碼塊(前向和後向都包含)。這意味着雖然在變量或常量聲明之前引用它是不合法的,但即使将示例中的x初始化移動到方法的末尾我們也會得到相同的錯誤,這個奇怪的規則和C++是不同的。

2.11.2 表達式語句

表達式語句既是表達式也是合法的語句。表達式語句必須改變狀态或者執行某些可能改變狀态的調用。狀态改變本質上指改變一個變量的值。可能的表達式語句有:

指派表達式(包括自增和自減表達式)

(有傳回值的和沒有傳回值的)方法調用表達式

對象執行個體化表達式

// Declare variables with declaration statements:
string s;
int x, y;
System.Text.StringBuilder sb;

// Expression statements
x = 1 + 2;                 // Assignment expression
x++;                       // Increment expression
y = Math.Max (x, 5);       // Assignment expression
Console.WriteLine (y);     // Method call expression
sb = new StringBuilder();  // Assignment expression
new StringBuilder();       // Object instantiation expression           

當調用有傳回值的構造器或方法時,并不一定要使用它的傳回值。是以除非構造器或方法改變了某些狀态,否則以下這些語句完全沒有用處:

new StringBuilder();     // Legal, but useless
new string ('c', 3);     // Legal, but useless
x.Equals (y);            // Legal, but useless
           

2.11.3 選擇語句

C#使用以下幾種機制來有條件地控制程式的執行流:

選擇語句(if、switch)

條件語句(?:)

循環語句(while、do..while、for和foreach)

本節介紹了兩種最簡單的結構:if-else語句和switch語句。

2.11.3.1 if語句

if語句在bool表達式為真時執行其中的語句。例如:

if (5 < 2 * 3)

Console.WriteLine ("true"); // true

if中的語句可以是代碼塊:

{

Console.WriteLine ("true");

Console.WriteLine ("Let's move on!");

}

2.11.3.2 else子句

if語句之後可以緊跟else子句:

if (2 + 2 == 5)

Console.WriteLine ("Does not compute");

else

Console.WriteLine ("False"); // False

在else子句中,能嵌套另一個if語句:

if (2 + 2 == 4)

Console.WriteLine ("Computes");    // Computes           

2.11.3.3 用大括号改變執行流

else子句總是與它之前的語句塊中緊鄰的未配對的if語句結合。例如:

if (true)

if (false)

Console.WriteLine();           
Console.WriteLine ("executes");           

語義上等價于:

Console.WriteLine();           
Console.WriteLine ("executes");           

可以通過改變大括号的位置來改變執行流:

Console.WriteLine();           

Console.WriteLine ("does not execute");

大括号可以明确表明結構,這能提高嵌套if語句的可讀性(雖然編譯器并不需要)。需要特别指出的是下面的模式:

static void TellMeWhatICanDo (int age)
{
  if (age >= 35)
    Console.WriteLine ("You can be president!");
  else if (age >= 21)
    Console.WriteLine ("You can drink!");
  else if (age >= 18)
    Console.WriteLine ("You can vote!");
  else
    Console.WriteLine ("You can wait!");
}
           

這裡,我們參照其他語言的elseif結構(以及C#本身的#elif預處理指令)來安排if和else語句。Visual Studio自動識别這個模式并保持代碼縮進。從語義上講,緊跟着每一個if語句的else語句從功能上都是嵌套在else子句之中的。

2.11.3.4 switch語句

switch語句可以根據變量可能的取值來轉移程式的執行。switch語句可以擁有比嵌套if語句更加簡潔的代碼,因為switch語句僅僅需要一次表達式計算,例如:

static void ShowCard (int cardNumber)
{
  switch (cardNumber)
  {
    case 13:
      Console.WriteLine ("King");
      break;
    case 12:
      Console.WriteLine ("Queen");
      break;
    case 11:
      Console.WriteLine ("Jack");
      break;
    case -1:                         // Joker is -1
      goto case 12;                  // In this game joker counts as queen
    default:                         // Executes for any other cardNumber
      Console.WriteLine (cardNumber);
      break;
  }
}           

這個例子示範了最一般的情形,即針對常量的switch。當指定常量時,隻能指定内置的整數類型、bool、char、enum類型以及string類型。

每一個case子句結束時必須使用某種跳轉指令顯式指定下一個執行點(除非你的代碼本身就是一個無限循環)。這些跳轉指令有:

break(跳轉到switch語句的最後)

goto case x(跳轉到另外一個case子句)

goto default(跳轉到default子句)

其他的跳轉語句,例如return、throw、continue或者goto label

當多個值要執行相同的代碼時,可以按照順序列出共同的case條件:

switch (cardNumber)
{
  case 13:
  case 12:
  case 11:
    Console.WriteLine ("Face card");
    break;
  default:
    Console.WriteLine ("Plain card");
    break;
}
           

switch語句的這種特性可以寫出比多個if-else更加簡潔的代碼。

2.11.3.5 帶有模式的switch語句(C# 7)

C# 7開始支援按類型switch:

static void Main()
{
  TellMeTheType (12);
  TellMeTheType ("hello");
  TellMeTheType (true);
}

static void TellMeTheType (object x)   // object allows any type.
{
  switch (x)
  {
    case int i:
      Console.WriteLine ("It's an int!");
      Console.WriteLine ($"The square of {i} is {i * i}");
      break;
    case string s:
      Console.WriteLine ("It's a string");
      Console.WriteLine ($"The length of {s} is {s.Length}");
      break;
    default:
      Console.WriteLine ("I don't know what x is");
      break;
  }
}           

(object類型允許其變量為任何類型。這部分内容将在3.2節和3.3節詳細讨論。)

每一個case子句都指定了一種需要比對的類型和一個變量(模式變量),如果類型比對成功就對變量指派。和常量不同,對于類型的使用并沒有任何限制。

還可以使用when關鍵字對case進行預測,例如:

switch (x)
{
  case bool b when b == true:     // Fires only when b is true
    Console.WriteLine ("True!");
    break;
  case bool b:
    Console.WriteLine ("False!");
    break;
}
           

case子句的順序會影響類型的選擇(這和選擇常量的情況有些不同)。如果交換case的順序,則上述示例可以得到完全不同的結果(事實上,上述程式甚至無法編譯,因為編譯器發現第二個case子句是永遠不會執行的)。但default子句是一個例外,不論它出現在什麼地方都會在最後才執行。

堆疊多個case子句也是沒有問題的。下面的例子中,Console.WriteLine會在任何浮點類型的值大于1000時執行:

switch (x)
{
  case float f when f > 1000:
  case double d when d > 1000:
  case decimal m when m > 1000:
    Console.WriteLine ("We can refer to x here but not f or d or m");
    break;
}
           

上述例子中,編譯器僅允許在when子句中使用模式變量f、d和m。當調用Console.WriteLine時,我們并不清楚到底三個模式變量中的哪一個會被指派,因而編譯器會将它們放在作用域之外。

除此以外,還可以混合使用常量選擇和模式選擇,甚至可以選擇null值:

case null:
  Console.WriteLine ("Nothing here");
  break;
           

2.11.4 疊代語句

C#中可以使用while、do-while、for和foreach語句重複執行一系列語句。

2.11.4.1 while和do-while循環

while循環在其bool表達式為true的情況下重複執行循環體中的代碼。這個表達式在循環體執行之前進行檢測。例如:

int i = 0;

while (i < 3)

Console.WriteLine (i);

i++;

OUTPUT:

1

2

do-while循環在功能上不同于while循環的地方是它在語句塊執行之後才檢查表達式的值(保證語句塊至少執行過一次)。以下将上述例子用do-while循環重新書寫了一遍:

do

while (i < 3);

2.11.4.2 for循環

for循環就像一個有特殊子句的while循環。這些特殊子句用于初始化和疊代循環變量。for循環有以下三個子句:

for (initialization-clause; condition-clause; iteration-clause)

statement-or-statement-block

初始化子句:在循環之前執行,初始化一個或多個疊代變量。

條件子句:它是一個bool表達式,當其為true時,将執行循環體。

疊代子句:在每次語句塊疊代之後執行,通常用于更新疊代變量。

例如,下面的例子将列印0到2的數字:

for (int i = 0; i < 3; i++)

下面的代碼将列印前10個斐波那契數(每一個數都是前面兩個數的和):

for (int i = 0, prevFib = 1, curFib = 1; i < 10; i++)

Console.WriteLine (prevFib);

int newFib = prevFib + curFib;

prevFib = curFib; curFib = newFib;

for語句的這三個部分都可以省略,因而可以通過下面的代碼來實作無限循環(也可以用while (true)來代替):

for (;;)

Console.WriteLine ("interrupt me");

2.11.4.3 foreach循環

foreach語句周遊可枚舉對象的每一個元素。大多數C#和.NET Framework中表示集合或元素清單的類型都是可枚舉的。例如,數組和字元串都是可枚舉的。以下示例從頭到尾枚舉了字元串中的每一個字元:

foreach (char c in "beer") // c is the iteration variable

Console.WriteLine (c);

b

e

r

我們将在4.6節詳細介紹。

2.11.5 跳轉語句

C#的跳轉語句有break、continue、goto、return和throw。

跳轉語句仍然遵守try語句的可靠性規則(參見4.5節)。這意味着:

到try語句塊之外的跳轉總是在達到目标之前執行try語句的finally語句塊。

跳轉語句不能從finally語句塊内跳到塊外(除非使用throw)。

2.11.5.1 break語句

break語句用于結束疊代或switch語句的執行:

int x = 0;

while (true)

if (x++ > 5)

break ;      // break from the loop           

// execution continues here after break

...

2.11.5.2 continue語句

continue語句放棄循環體中其後的語句,繼續下一輪疊代。例如,以下的循環跳過了偶數:

for (int i = 0; i < 10; i++)

if ((i % 2) == 0) // If i is even,

continue;             // continue with next iteration
           

Console.Write (i + " ");

OUTPUT: 1 3 5 7 9

2.11.5.3 goto語句

goto語句将執行點轉移到語句塊中的指定标簽處。格式如下:

goto statement-label;

或用于switch語句内:

goto case case-constant; // (Only works with constants, not patterns)

标簽語句僅僅是代碼塊中的占位符,位于語句之前,用冒号字尾表示。下面的代碼模拟for循環來周遊從1到5的數字:

int i = 1;

startLoop:

if (i <= 5)

Console.Write (i + " ");

goto startLoop;

OUTPUT: 1 2 3 4 5

goto case case-constant會将執行點轉移到switch語句塊中的另一個條件上(參見本章2.11.3.4節)。

2.11.5.4 return語句

return語句用于退出方法。如果這個方法有傳回值,則必須傳回方法指定傳回類型的表達式。

static decimal AsPercentage (decimal d)

decimal p = d * 100m;

return p; // Return to the calling method with value

return語句能夠出現在方法的任意位置(除finally塊中)。

2.11.5.5 throw語句

throw語句抛出異常來表示有錯誤發生(參見4.5節):

if (w == null)

throw new ArgumentNullException (...);

2.11.6其他語句

using語句用一種優雅的文法在finally塊中調用實作了IDisposable接口對象的Dispose方法。(請參見4.5節和12.1節)

C#重載了using關鍵字,使它在不同上下文中有不同的含義。特别注意using指令和using語句是不同的。

lock語句是調用Mintor類型的Enter和Exit方法的簡化寫法。(請參見第14章和第23章。)

2.12 命名空間

命名空間是一系列類型名稱的領域。通常情況下,類型組織在分層的命名空間裡,既避免了命名沖突又更容易查找。例如,處理公鑰加密的RSA類型就定義在如下的命名空間下:

System.Security.Cryptography

命名空間組成了類型名的基本部分。下面代碼調用了RSA類型的Create方法:

System.Security.Cryptography.RSA rsa =
  System.Security.Cryptography.RSA.Create();
           

命名空間是獨立于程式集的。程式集是像.exe或者.dll一樣的部署單元(參見第18章)。命名空間并不影響成員的public、internal、private的可見性。

namespace關鍵字為其中的類型定義了命名空間。例如:

namespace Outer.Middle.Inner
{
  class Class1 {}
  class Class2 {}
}
           

命名空間中的“.”表明了嵌套命名空間的層次結構。下面的代碼在語義上和上一個例子是等價的:

namespace Outer
{
  namespace Middle
  {
    namespace Inner
    {
      class Class1 {}
      class Class2 {}
    }
  }
}
           

類型可以用完全限定名稱(fully qualified name),也就是包含從外到内的所有命名空間的名稱,來指定。例如,上述例子中,可以使用Outer.Middle.Inner.Class1來指代Class1。

如果類型沒有在任何命名空間中定義,則它存在于全局命名空間(global namespace)中。全局命名空間也包含了頂級命名空間,就像前面例子中的Outer命名空間。

2.12.1 using指令

using指令用于導入命名空間。這是避免使用完全限定名稱來指代某種類型的快捷方法。以下例子導入了前一個例子的Outer.Middle.Inner命名空間:

using Outer.Middle.Inner;

class Test
{
  static void Main()
  {
    Class1 c;    // Don't need fully qualified name
  }
}
           

在不同命名空間中定義相同類型名稱是合法的(而且通常是需要的)。然而,這種做法通常出現在開發者不會同時導入兩個命名空間時。在.NET Framework中的TextBox類就是一個典型的例子。這個名稱在System.Windows.Controls(WPF)和System.Web.UI.WebControls(ASP.NET)命名空間中都有定義。

2.12.2 using static指令(C# 6)

從C# 6開始,我們不僅可以導入命名空間還可以使用using static指令導入特定的類型。這樣就可以類型接使用類型靜态成員而不需要指定類型的名稱了。在接下來的例子中,我們這樣調用Console類的靜态方法WriteLine:

using static System.Console;

class Test

static void Main() { WriteLine ("Hello"); }

using static指令将類型的可通路的靜态成員,包括字段、屬性以及嵌套類型(參見第3章),全部導入進來。同時,該指令也支援導入枚舉類型的成員(見第3章)。是以如果導入了以下的枚舉類型:

using static System.Windows.Visibility;

我們就可以直接使用Hidden而不是Visibility.Hidden了:

var textBox = new TextBox { Visibility = Hidden }; // XAML-style

C#編譯器還沒有聰明到可以基于上下文來推斷出正确的類型,是以在導入多個靜态類型導緻二義性時會發生編譯錯誤。

2.12.3 命名空間中的規則

2.12.3.1 名稱範圍

外層命名空間中聲明的名稱能夠直接在内層命名空間中使用。以下示例中的Class1在Inner中不需要限定名稱:

namespace Outer
{
  class Class1 {}

  namespace Inner
  {
    class Class2 : Class1  {}
  }
}
           

使用統一命名空間分層結構中不同分支的類型需要使用部分限定名稱。在下面的例子中,SalesReport類繼承Common.ReportBase:

namespace MyTradingCompany
{
  namespace Common
  {
    class ReportBase {}
  }
  namespace ManagementReporting
  {
    class SalesReport : Common.ReportBase  {}
  }
}
           

2.12.3.2 名稱隐藏

如果相同類型名稱同時出現在内層和外層命名空間中,則内層類型優先。如果要使用外層命名空間中的類型,必須使用它的完全限定名稱。

namespace Outer
{
  class Foo { }

  namespace Inner
  {
    class Foo { }

    class Test
    {
      Foo f1;         // = Outer.Inner.Foo
      Outer.Foo f2;   // = Outer.Foo
    }
  }
}
           

所有的類型名在編譯時都會轉換為完全限定名稱。中間語言(IL)代碼不包含非限定名稱和部分限定名稱。

2.12.3.3 重複的命名空間

隻要命名空間内的類型名稱不沖突就可以重複聲明同一個命名空間:

namespace Outer.Middle.Inner
{
  class Class1 {}
}

namespace Outer.Middle.Inner
{
  class Class2 {}
}
           

上述例子也可以分為兩個不同的源檔案,并将每一個類都編譯到不同的程式集中。

源檔案1:

namespace Outer.Middle.Inner

class Class1 {}

源檔案2:

class Class2 {}

2.12.3.4 嵌套的using指令

我們能夠在命名空間中嵌套使用using指令,這樣可以控制using指令在命名空間聲明中的作用範圍。在以下例子中,Class1在一個命名空間中可見,但是在另一個命名空間中不可見:

namespace N1
{
  class Class1 {}
}

namespace N2
{
  using N1;

  class Class2 : Class1 {}
}

namespace N2
{
  class Class3 : Class1 {}   // Compile-time error
}           

2.12.4 類型和命名空間别名

導入命名空間可能導緻類型名稱的沖突,是以可以隻導入需要的特定類型而不是整個命名空間,并給它們建立别名。例如:

using PropertyInfo2 = System.Reflection.PropertyInfo;

class Program { PropertyInfo2 p; }

下面代碼為整個命名空間建立别名:

using R = System.Reflection;

class Program { R.PropertyInfo p; }

2.12.5 進階命名空間特性

2.12.5.1 外部别名

使用外部别名就可以引用兩個完全限定名稱相同的類型(例如,命名空間和類型名稱都相同)。這種特殊情況隻在兩種類型來自不同的程式集時才會出現。請考慮下面的例子:

程式庫1:

// csc target:library /out:Widgets1.dll widgetsv1.cs

namespace Widgets
{
  public class Widget {}
}
           

程式庫2:

// csc target:library /out:Widgets2.dll widgetsv2.cs

namespace Widgets
{
  public class Widget {}
}
           

應用程式:

// csc /r:Widgets1.dll /r:Widgets2.dll application.cs

using Widgets;

class Test
{
  static void Main()
  {
    Widget w = new Widget();
  }
}
           

這個應用程式無法編譯,因為Widget類型是有二義性的。外部别名則可以消除應用程式中的二義性:

// csc /r:W1=Widgets1.dll /r:W2=Widgets2.dll application.cs

extern alias W1;
extern alias W2;

class Test
{
  static void Main()
  {
    W1.Widgets.Widget w1 = new W1.Widgets.Widget();
    W2.Widgets.Widget w2 = new W2.Widgets.Widget();
  }
}
           

2.12.5.2 命名空間别名限定符

之前提到,内層命名空間中的名稱隐藏外層命名空間中的名稱。但是,有時即使使用類型的完全限定名也無法解決沖突。請考慮下面的例子:

namespace N
{
  class A
  {
    public class B {}                    // Nested type
    static void Main() { new A.B(); }    // Instantiate class B
  }
}

namespace A
{
  class B {}
}
           

Main方法将會執行個體化嵌套類B或命名空間A中的類B。編譯器總是給目前命名空間中的辨別符以更高的優先級;在這種情況下,将會執行個體化嵌套類B。

要解決這樣的沖突,可以使用如下的方式限定命名空間中的名稱:

全局命名空間,即所有命名空間的根命名空間(由上下文關鍵字global指定)

一系列的外部别名

“::”用于限定命名空間别名。下面的例子中,我們使用了全局命名空間(這通常出現在自動生成的代碼中,以避免名稱沖突)

namespace N
{
  class A
  {
    static void Main()
    {
      System.Console.WriteLine (new A.B());
      System.Console.WriteLine (new global::A.B());
    }

    public class B {}
  }
}

namespace A
{
  class B {}
}           

以下例子使用了别名限定符(2.12.5.1一節中例子的修改版本):

extern alias W1;
extern alias W2;

class Test
{
  static void Main()
  {
    W1::Widgets.Widget w1 = new W1::Widgets.Widget();
    W2::Widgets.Widget w2 = new W2::Widgets.Widget();
  }
}           

繼續閱讀