C# 程式設計/變數
變數 用於儲存值。更準確地說,一個變數 繫結 一個 物件(在術語的普遍意義上,即一個特定值)到一個識別符號(變數的名稱)以便以後可以訪問該物件。例如,變數可以儲存一個值以便以後使用
string name = "Dr. Jones";
Console.WriteLine("Good morning " + name);
在這個例子中,“name”是識別符號,“Dr. Jones”是我們繫結到它的值。此外,每個變數都用顯式的型別宣告。只有型別與變數宣告型別相容的值才能繫結到(儲存在)變數中。在上面的例子中,我們將“Dr. Jones”儲存到一個名為 string 的型別變數中。這是一個合法的語句。但是,如果我們說 int name = "Dr. Jones",編譯器會丟擲一個錯誤,告訴我們不能在 int 和 string 之間進行隱式轉換。有一些方法可以做到這一點,但我們將在以後討論它們。
C# 支援多個與變數的通用程式設計概念相對應的程式元素:欄位、引數和區域性變數。
欄位,有時稱為類級變數,是與類或結構體關聯的變數。例項變數是與類或結構體例項關聯的欄位,而靜態變數,用 static 關鍵字宣告,是與型別本身關聯的欄位。欄位也可以透過將它們設為常量 (const) 來與它們的類關聯,這需要對常量值進行宣告賦值,並防止隨後對該欄位進行更改。
每個欄位的可見性為公共、受保護、內部、受保護的內部或私有(從最可見到最不可見)。
與欄位類似,區域性變數可以選擇設定為常量 (const)。常量區域性變數儲存在程式集資料區域中,而非常量區域性變數儲存在(或引用自)堆疊上。因此,它們既有宣告它們的函式或語句塊的範圍,也有其擴充套件。
引數是與函式關聯的變數。
輸入引數的值可以從呼叫者傳遞到函式的環境中,因此函式對該引數的更改不會影響呼叫者的變數的值,也可以透過引用傳遞,因此對變數的更改將影響呼叫者的變數的值。值型別(int、double、string)按“值傳遞”,而引用型別(物件)按“引用傳遞”。由於這是 C# 編譯器的預設設定,因此不需要使用 '&',就像在 C 或 C++ 中一樣。
輸出引數不會複製其值,因此在函式環境中對變數值的更改會直接影響呼叫者環境中的值。在函式進入時,編譯器將此類變數視為未繫結,因此在給它賦值之前引用輸出引數是非法的。為了使函式能夠編譯,它還必須在函式中每個有效(非異常)程式碼路徑中被賦值。
引用引數類似於輸出引數,但它在函式呼叫之前繫結,並且不需要由函式賦值。
params 引數表示可變數量的引數。如果方法簽名中包含一個params 引數,則params 引數必須是簽名中的最後一個引數。
// Each pair of lines is what the definition of a method and a call of a
// method with each of the parameters types would look like.
// In param:
void MethodOne(int param1) // definition
MethodOne(variable); // call
// Out param:
void MethodTwo(out string message) // definition
MethodTwo(out variable); // call
// Reference param;
void MethodThree(ref int someFlag) // definition
MethodThree(ref theFlag) // call
// Params
void MethodFour(params string[] names) // definition
MethodFour("Matthew", "Mark", "Luke", "John"); // call
C# 中的每個型別要麼是值型別,要麼是引用型別。C# 擁有幾種預定義(“內建”)型別,並允許宣告自定義值型別和引用型別。
值型別和引用型別之間存在根本差異:值型別分配在堆疊上,而引用型別分配在堆上。
.NET 框架中的值型別通常是小型、常用的型別。使用它們的優勢在於,該型別需要很少的資源才能由 CLR 啟動和執行。值型別不需要在堆上分配記憶體,因此不會導致垃圾回收。但是,為了發揮作用,值型別(或從它派生的型別)應該保持較小 - 理想情況下應該低於 16 位元組的資料。如果選擇讓值型別變得更大,建議不要將其傳遞給函式(這可能需要複製其所有欄位),也不要將其從函式中返回。
雖然這聽起來像一個有用的型別,但它確實有一些缺陷,在使用它時需要了解這些缺陷。
- 值型別在傳遞給函式之前總是被複制(內在地)。對這個新物件的更改不會反映到傳遞給函式的原始物件中。
- 值型別不需要呼叫它們的建構函式。它們會自動初始化。
- 值型別總是將其欄位初始化為 0 或 null。
- 值型別永遠不能被賦值為 null(但可以使用可空型別)。
- 值型別有時需要裝箱(包裝在一個物件中),允許它們的值像物件一樣使用。
CLR 以非常不同的方式管理引用型別。所有引用型別都包含兩個部分:一個指向堆的指標(包含該物件)以及物件本身。引用型別略微更重,因為幕後管理需要跟蹤它們。然而,這對於在傳遞指標而不是複製值到/從函式的靈活性以及速度提升來說是一個小代價。
當使用建構函式初始化一個引用型別物件時,CLR 需要執行以下四個操作。
- CLR 計算在堆上儲存物件所需的記憶體量。
- CLR 將資料插入到新建立的記憶體空間中。
- CLR 標記空間的結束位置,以便可以將下一個物件放置在那裡。
- CLR 返回對新建立空間的引用。
每次建立物件時都會發生這種情況。但是,假設記憶體是無限的,因此需要進行一些維護 - 這就是垃圾收集器發揮作用的地方。
由於 C# 中的型別系統與其他符合 CLI 的語言統一,因此每個 C# 整型實際上都是 .NET Framework 中對應型別的別名。雖然別名的名稱在 .NET 語言之間有所不同,但 .NET Framework 中的底層型別保持不變。因此,在其他 .NET Framework 語言編寫的程式集中建立的物件可以繫結到 C# 變數,這些變數的型別是根據下面的轉換規則可以轉換到的任何型別。以下內容透過將 C# 程式碼與等效的 Visual Basic .NET 程式碼進行比較,說明了型別的跨語言相容性。
// C#
public void UsingCSharpTypeAlias()
{
int i = 42;
}
public void EquivalentCodeWithoutAlias()
{
System.Int32 i = 42;
}
' Visual Basic .NET
Public Sub UsingVisualBasicTypeAlias()
Dim i As Integer = 42
End Sub
Public Sub EquivalentCodeWithoutAlias()
Dim i As System.Int32 = 42
End Sub
使用特定於語言的類型別名通常被認為比使用完全限定的 .NET Framework 型別名稱更具可讀性。
每個 C# 型別都對應於統一型別系統中的一個型別,這一事實使每個值型別在跨平臺和編譯器之間具有一致的大小。這種一致性是與其他語言(如 C)的重要區別,在 C 中,例如,一個long僅保證至少與一個int一樣大,並且由不同的編譯器以不同的大小實現。作為引用型別,從object派生的型別的變數(即任何class)不受一致大小要求的限制。也就是說,引用型別的尺寸,如System.IntPtr,而不是值型別,如System.Int32,可能因平臺而異。幸運的是,很少需要知道引用型別的實際大小。
有兩個預定義的引用型別:object,它是System.Object類的別名,所有其他引用型別都從它派生;以及string,它是System.String類的別名。C# 同樣具有幾個整型值型別,每個都是 .NET Framework 的System名稱空間中對應值型別的別名。預定義的 C# 類型別名公開了底層 .NET Framework 型別的函式。例如,由於 .NET Framework 的System.Int32型別實現了一個ToString()函式來將整數的值轉換為其字串表示形式,因此 C# 的int型別公開了該函式。
int i = 97;
string s = i.ToString(); // The value of s is now the string "97".
同樣,System.Int32型別實現了Parse()函式,因此可以透過 C# 的int型別訪問它。
string s = "97";
int i = int.Parse(s); // The value of i is now the integer 97.
統一型別系統透過將值型別轉換為引用型別(裝箱)以及將某些引用型別轉換為其對應值型別(拆箱)的能力得到了增強。這也被稱為強制轉換。
object boxedInteger = 97;
int unboxedInteger = (int) boxedInteger;
但是,裝箱和強制轉換不是型別安全的:如果程式設計師混淆了型別,編譯器不會生成錯誤。在以下簡短示例中,錯誤非常明顯,但在複雜的程式中,可能很難發現。如果可能,避免裝箱。
object getInteger = "97";
int anInteger = (int) getInteger; // No compile-time error. The program will crash, however.
內建的 C# 類型別名及其等效的 .NET Framework 型別如下所示。
| C# 別名 | .NET 型別 | 大小(位) | 範圍 |
|---|---|---|---|
| sbyte | System.SByte | 8 | -128 到 127 |
| byte | System.Byte | 8 | 0 到 255 |
| short | System.Int16 | 16 | -32,768 到 32,767 |
| ushort | System.UInt16 | 16 | 0 到 65,535 |
| char | System.Char | 16 | 程式碼為 0 到 65,535 的 Unicode 字元 |
| int | System.Int32 | 32 | -2,147,483,648 到 2,147,483,647 |
| uint | System.UInt32 | 32 | 0 到 4,294,967,295 |
| long | System.Int64 | 64 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
| ulong | System.UInt64 | 64 | 0 到 18,446,744,073,709,551,615 |
| C# 別名 | .NET 型別 | 大小(位) | 精度 | 範圍 |
|---|---|---|---|---|
| float | System.Single | 32 | 7 位 | 1.5 x 10-45 到 3.4 x 1038 |
| double | System.Double | 64 | 15-16 位 | 5.0 x 10-324 到 1.7 x 10308 |
| decimal | System.Decimal | 128 | 28-29 個小數位 | 1.0 x 10-28 到 7.9 x 1028 |
| C# 別名 | .NET 型別 | 大小(位) | 範圍 |
|---|---|---|---|
| bool | System.Boolean | 32 | true 或 false,它們與 C# 中的任何整數無關。 |
| object | System.Object | 32/64 | 與平臺相關(指向物件的指標)。 |
| string | System.String | 16*length | 沒有特殊上限的 Unicode 字串。 |
預定義型別可以聚合和擴充套件為自定義型別。
自定義值型別用struct 或 enum 關鍵字宣告。同樣,自定義引用型別 用 class 關鍵字宣告。
儘管陣列宣告中包含維數,但沒有包含每個維的大小。
string[] a_str;
但是,對陣列變數(在變數使用之前)的賦值指定了每個維的大小。
a_str = new string[5];
與其他變數型別一樣,宣告和初始化可以組合起來。
string[] a_str = new string[5];
同樣重要的是要注意,就像在 Java 中一樣,陣列是按引用傳遞的,而不是按值傳遞的。例如,以下程式碼片段成功地交換了整數陣列中的兩個元素。
static void swap (int[] a_iArray, int iI, int iJ)
{
int iTemp = a_iArray[iI];
a_iArray[iI] = a_iArray[iJ];
a_iArray[iJ] = iTemp;
}
可以在執行時確定陣列大小。以下示例將迴圈計數器分配給無符號短整型陣列元素。
ushort[] a_usNumbers = new ushort[234];
[...]
for (ushort us = 0; us < a_usNumbers.Length; us++)
{
a_usNumbers[us] = us;
}
從 C# 2.0 開始,可以在結構中包含陣列。
using System;
namespace Login
{
class Username_Password
{
public static void Main()
{
string username,password;
Console.Write("Enter username: ");
username = Console.ReadLine();
Console.Write("Enter password: ");
password = Console.ReadLine();
if (username == "SomePerson" && password == "SomePassword")
{
Console.WriteLine("Access Granted.");
}
else if (username != "SomePerson" && password == "SomePassword")
{
Console.WriteLine("The username is wrong.");
}
else if (username == "SomePerson" && password != "SomePassword")
{
Console.WriteLine("The password is wrong.");
}
else
{
Console.WriteLine("Access Denied.");
}
}
}
}
根據預定義的轉換規則、繼承結構和顯式強制轉換定義,給定型別的數值可能可以或不可以顯式或隱式轉換為其他型別。
許多預定義的值型別都具有到其他預定義值型別的預定義轉換。如果型別轉換保證不會丟失資訊,則轉換可以是 *隱式* 的(即,不需要顯式的 *強制轉換*)。
可以將值隱式轉換為它繼承的任何類或它實現的介面。要將基類轉換為從它繼承的類,轉換必須是顯式的,以便轉換語句能夠編譯。類似地,要將介面例項轉換為實現它的類,轉換必須是顯式的,以便轉換語句能夠編譯。在這兩種情況下,如果要轉換的值不是目標型別的例項或其任何派生型別,執行時環境都會丟擲轉換異常。
變數的範圍和範圍基於它們的宣告。引數和區域性變數的範圍對應於宣告的方法或語句塊,而欄位的範圍與例項或類相關聯,並且可能被欄位的訪問修飾符進一步限制。
變數的範圍由執行時環境使用隱式引用計數和複雜的垃圾回收演算法確定。