本文討論 Java 和 C# 之間的異同點,目的在於當遷移到 .NET 時,讓 Java 開發人員掌握所涉及的一些知識。Java 和 C# 之間的主要相似點是:
• Java 和 C# 都源於 C++,並且共有 C++ 的一些特征。 • 兩種語言都需要編譯成中間代碼,而不是直接編譯成純機器碼。Java 編譯成 Java 虛擬機 (Java Virtual Machine, JVM) 字節碼,而 C# 則編譯成公共中間語言 (Common Intermediate Language, CIL)。 • Java 字節碼是通過稱為 Java 虛擬機 (JVM) 的應用程序執行的。類似地,已編譯的 C# 程序由公共語言運行庫 (Common Language Runtime, CLR) 執行。 • 除了一些細微的差別以外,C# 中的異常處理與 Java 非常相似。C# 用 try...catch 構造來處理運行時錯誤(也稱為異常),這和 Java 中是完全一樣的。System.Exception 類是所有 C# 異常類的基類。 • 同 Java 一樣,C# 是強類型檢查編程語言。編譯器能夠檢測在運行時可能會出現問題的類型錯誤。 • 同 Java 一樣,C# 提供自動垃圾回收功能,從而使編程人員避免了跟蹤分配的資源。 • Java 和 C# 都支持單一繼承和多接口實現。現在讓我們看一看本文涵蓋的重要差異:
• C# 語言引入了一些新的語言構造,如 foreach、索引器、屬性、委托、運算符重載和其他語言構造。在本文後面我們將更詳細地討論這些構造。源文件約定
我們需要知道,兩種語言在源程序的文件命名約定和結構上有一些不同:
文件命名
包含 C# 類的文件的命名約定與 Java 有點不同。首先,在 Java 中,所有源文件的擴展名都為 .java。每個源文件都包含一個頂層公共類聲明,並且類名必須與文件名相匹配。換句話說,一個用公共范圍聲明的名為 Customer 的類必須定義在具有名稱 Customer.java 的源文件中。
而 C# 源代碼是由 .cs 擴展名表示的。與 Java 不同,源文件可以包含多個頂層公共類聲明,而文件名不需要與任何類名相匹配。
頂層聲明
在 Java 和 C# 中,源代碼以按一定順序排列的頂層聲明開始。Java 和 C# 程序中的聲明只存在少許差別。
Java 中的頂層聲明
在 Java 中,我們可以用 package 關鍵字將類組合在一起。打包的類必須在源文件的第一個可執行的行中使用 package 關鍵字。接著出現的是需要訪問其他包中的類的任何導入語句,之後是類聲明,比如:
package ;
import .;
class Customer
{
...
}
C# 中的頂層聲明
C# 使用命名空間的概念,通過 namespace 關鍵字將邏輯上相關的類組合在一起。這些做法類似於 Java 包,而具有相同名稱的類可以出現在兩個不同的命名空間中。要訪問定義在當前命名空間之外的命名空間中的類,我們可以使用緊跟該命名空間名的 using 關鍵字,如下所示:
using .;
namespace
{
class Customer
{
...
}
}
注意,using 語句可以完全合法地放在命名空間聲明中,在這種情況下,這樣導入的命名空間就形成了包含命名空間的一部分。
Java 不允許在相同的源文件中有多個包,而 C# 允許在一個 .cs 文件中有多個命名空間:
namespace AcmeAccounting
{
public class GetDetails
{
...
}
}
namespace AcmeFinance
{
public class ShowDetails
{
...
}
}
完全限定名和命名空間別名
同 Java 一樣,通過提供類的完全限定名(如 System.Data.DataSet 或上面的示例中的 AcmeAccounting.GetDetails),我們可以在沒有命名空間的 using 引用的情況下訪問 .NET 或用戶定義的命名空間中的類。
完全限定名可能會變得很長而不便於使用,在這種情況下,我們可以使用 using 關鍵字來指定一個簡稱或別名,以提高代碼的可讀性。
在下面的代碼中,創建了一個別名來引用由一個虛構的公司所編寫的代碼:
using DataTier = Acme.SQLCode.Client;
using System;
public class OutputSales
{
public static void Main()
{
int sales = DataTier.GetSales("January");
Console.WriteLine("January's Sales: {0}", sales);
}
}
注意 WriteLine() 的語法,格式字符串中帶有 {x},其中 x 表示在此處要插入的值的參數列表的位置。假定 GetSales() 方法返回 500,則該應用程序的輸出將為:
January's Sales: 500
預處理指令
與 C 和 C++ 相似,C# 包括預處理器指令,預處理器指令提供了有條件地跳過源文件的某些部分、報告錯誤和警告條件,以及描述源代碼的不同部分的能力。使用“預處理指令”這個術語只是為了與 C 和 C++ 編程語言保持一致,因為 C# 並不包括單獨的預處理步驟。有關 C# 預處理器指令的完整列表
語言語法
在這一部分中,我們討論這兩種語言之間的相似點和不同點。一些主要的不同點有:
•
常量聲明— Java 為此而使用 final 關鍵字,而 C# 使用關鍵字 const 或 readonly。
•
復合數據類型— 在 Java 中,我們可以使用類關鍵字來創建作為沒有方法的類的復合數據類型,但是 C# 為此提供了 struct,同 C 中一樣。
•
析構函數— C# 允許我們創建在銷毀類的實例之前調用的析構函數方法。在 Java 中,可以提供 finalize() 方法來包含在將對象作為垃圾回收之前清除資源的代碼。在 C# 中,由類析構函數來提供此功能。析構函數類似一個沒有參數並前面帶有波形符“~”的構造函數。
•
函數指針 — C# 提供一個稱為 delegate 的構造來創建類型安全的函數指針。Java 沒有任何與之對等的機制。
數據類型
C# 提供了在 Java 中可用的所有數據類型,並且增加了對無符號數和新的 128 位高精度浮點類型的支持。
在 Java 中,對於每個基本數據類型,核心類庫都提供了一個包裝類來將其表示為 Java 對象。例如,Integer 類包裝 int 數據類型,而 Double 類包裝 double 數據類型。
而在 C# 中,所有的基本數據類型都是 System 命名空間中的對象。對於每個數據類型,都提供一個簡稱或別名。例如,int 是 System.Int32 的簡稱,而 double 是 System.Double 的簡寫形式。
下面的列表給出了 C# 數據類型及其別名。可以看到,前 8 個對應於 Java 中可用的基本類型。不過,請注意,Java 的 boolean 在 C# 中稱為 bool。
C# 數據類型
簡稱
.NET類
類型
寬度
范圍(位)
byte
System.Byte
無符號整數
8
-128 到 127
sbyte
System.SByte
有符號整數
8
-128 到 127
int
System.Int32
有符號整數
32
-2,147,483,648 到 2,147,483,647
uint
System.UInt32
無符號整數
32
0 到 4294967295
short
System.Int16
有符號整數
16
-32,768 到 32,767
ushort
System.UInt16
無符號整數
16
0 到 65535
long
System.Int64
有符號整數
64
-922337203685477508 到 922337203685477507
ulong
System.UInt64
無符號整數
64
0 到 18446744073709551615
float
System.Single
單精度浮點類型
32
-3.402823e38 到 3.402823e38
double
System.Double
雙精度浮點類型
64
-1.79769313486232e308 到 1.79769313486232e308
char
System.Char
單個 Unicode 字符
16
用在文本中的 Unicode 符號
bool
System.Boolean
邏輯 Boolean 類型
8
true 或 false
object
System.Object
所有其他類型的基本類型
string
System.String
字符序列
decimal
System.Decimal
可以表示具有 29 個有效位的小數的精確分數或整數類型
128
-2 x 10-96 到 2 x 1096
因為 C# 將所有的基本數據類型都表示為對象,所以按照基本數據類型來調用對象方法是可能的。例如:
int i=10;
Console.WriteLine(i.ToString());
借助於自動裝箱和拆箱,可以達到此目的。更多信息請參見。
枚舉
與 C/C++ 相似,在 C# 中可以使用枚舉來組合已命名常量,而在 Java 中不能使用枚舉。下面的示例定義了一個簡單的 Color 枚舉。
public enum Color {Green, Orange, Red, Blue}
還可以為枚舉賦整數值,如下面的枚舉聲明所示:
public enum Color {Green=10, Orange=20, Red=30, Blue=40}
下面的程序調用 Enum 類型的 GetNames 方法來顯示枚舉的可用常量。然後,它將值賦給枚舉,並顯示該值。
using System;
public class TypeTest
{
public static void Main()
{
Console.WriteLine("Possible color choices: ");
//Enum.GetNames returns a string array of named constants for the enum
foreach(string s in Enum.GetNames(typeof(Color)))
{
Console.WriteLine(s);
}
Color FavoriteColor = Color.Blue;
Console.WriteLine("Favorite Color is {0}",FavoriteColor);
Console.WriteLine("Favorite Color value is {0}", (int)FavoriteColor);
}
}
在運行之後,該程序將顯示如下結果:
Possible color choices:
Green
Orange
Red
Blue
Favorite Color is Blue
Favorite Color value is 40
字符串
在 Java 和 C# 中,字符串類型表現出相似的行為,只有一些細微的差別。二者的字符串類型均是不可改變的,這意味著一旦字符串創建完畢,就不能改變字符串的值。在二者的實例中,看起來像修改字符串實際內容的方法實際上創建一個新的字符串供返回,而保留原始的字符串不變。在 C# 和 Java 中,比較字符串值的過程是不同的。在 Java 中,為了比較字符串的值,開發人員需要按照字符串類型調用 equals() 方法,正如在默認情況下 == 運算符比較引用類型一樣。在 C# 中,開發人員可以使用 == 或 != 運算符來直接比較字符串的值。在 C# 中,盡管字符串是引用類型,但是在默認情況下,== 和 != 運算符將比較字符串的值而不是引用。在本文後面,我們將討論值類型和引用。
正如在 Java 中一樣,C# 開發人員不應該使用字符串類型來連接字符串,以避免每次連接字符串時都創建新的字符串類的開銷。相反,開發人員可以使用 System.Text 命名空間中的 StringBuilder 類,它在功能上等同於 Java 中的 StringBuffer 類。
字符串
C# 提供了避免在字符串常量中使用轉義序列(如代表制表符的“\t”或代表反斜槓字符的“\”)的功能。要這樣做,可以在為字符串賦值之前使用 @ 符號來聲明字符串。下面的示例顯示了如何使用轉義字符以及如何為字符串賦值:
//Using escaped characters
string path = "\\\\FileShare\\Directory\\file.txt";
//Using String Literals
string escapedPath = @"\\FileShare\Directory\file.txt";
轉換和強制轉換
Java 和 C# 遵守相似的數據類型自動轉換和強制轉換規則。
同 Java 一樣,C# 既支持隱式類型轉換又支持顯式類型轉換。在擴大轉換的情況下,轉換是隱式的。例如,下面從 int 到 long 的轉換是隱式的,如同 Java 中的一樣:
int intVariable = 5;
long l = intVariable;
下面是 .NET 數據類型之間的隱式轉換列表:
隱式轉換 源類型 目標類型 byte short, ushort, int, uint, long, ulong, float, double 或 decimal sbyte short, int, long, float, double, ? decimal int long, float, double, 或 decimal uint long, ulong, float, double, 或 decimal short int, long, float, double, 或 decimal ushort int, uint, long, ulong, float, double, 或 decimal long float, double, 或 decimal ulong float, double, 或 decimal float double char ushort, int, uint, long, ulong, float, double, 或 decimal可以使用與 Java 一樣的語法對希望顯式轉換的表達式進行強制轉換:
long longVariable = 5483;
int intVariable = (int)longVariable;
值類型和引用類型
C# 支持兩種變量類型:
• 值類型 — 這些是內置的基本數據類型,例如 char、int、float 和用 struct 聲明的用戶定義類型。 • 引用類型 — 從基本類型構造而來的類和其他復雜數據類型。這種類型的變量並不包含類型的實例,而只是包含對實例的引用。讓我們略微深入地研究一下這個問題。如果我們創建兩個值類型變量 i 和 j,比如:
int i = 10;
int j = 20;
圖 1:值類型的內存位置
則 i 和 j 彼此完全獨立,並且分配了不同的內存位置:
如果我們改變這些變量中的某一個的值,另一個自然不會受到影響。例如,如果我們有一個這樣的表達式:
int k = i;
則變量之間仍然沒有聯系。也就是說,之後如果我們改變 i 的值,k 還會保留賦值時 i 具有的值。
然而,引用類型的做法卻不同。例如,我們可以這樣聲明兩個變量:
myClass a = new myClass();
myClass b = a;
現在,因為類是 C# 中的引用類型,所以 a 稱為對 myClass 的引用。上面兩行中的第一行在內存中創建了 myClass 的一個實例,並且將 a 設置為引用該實例。因此,當我們將 b 設置為等於 a 時,它就包含了對內存中類的引用的重復。如果我們現在改變 b 中的屬性,a 中的屬性就將反映這些改變,因為這兩者都指向內存中的相同對象,如下圖所示:
圖 2:引用類型的內存位置
裝箱 (Boxing) 和拆箱 (Unboxing)
這種將值類型轉換為引用類型的過程稱為裝箱。而相反的將引用類型轉換為值類型的過程就稱為拆箱。如下面的代碼所示:
int valueVariable = 10;
// boxing
object obj = refVariable;
// unboxing
int valueVariable = (int) refVariable;
Java 需要我們手動執行這樣的轉換。通過構造這樣的對象,可以將基本數據類型轉換成包裝類的對象(裝箱)。同樣,通過調用這種對象中的適當方法,也可以從包裝類的對象中提取基本數據類型的值(拆箱)。
運算符
C# 提供了 Java 支持的所有可用運算符,如下表所示。在表的末尾,您將看到一些新的運算符,它們可以在 C# 中使用而不可以在 Java 中使用:
運算符 類別 符號 [Text] [Text] 一元 ++ -- + - ! ~ () 乘法 * / % 加法 + - 移位 << >> 關系 < > <= >= instanceof 相等 == != 邏輯與 & 邏輯異或 ^ 邏輯或 | 條件與 && 條件或 || 條件 ? : 賦值 = *= /= %= += -= <<= >>= &= ^= |= 操作數的類型 typeof 操作數的大小 sizeof 執行溢出檢查 checked 取消溢出檢查 unchecked唯一不可以在 C# 中使用的 Java 運算符是 >>> 移位運算符。之所以在 Java 中存在此運算符,是因為該語言中缺乏無符號變量,例如在需要右移位以在最高有效比特位插入 1 時。
然而,C# 支持無符號變量,因而 C# 只需要標准 >> 運算符。取決於操作數是否帶有符號,此運算符產生不同的結果。右移一個無符號數會在最高有效比特位插入 0,而右移一個有符號數則會復制前面的最高有效比特位。
checked 和 unchecked 運算符
如果對於分配給正在使用的數據類型的比特數來說結果太大,則算術運算會產生溢出。對於特定的整數算術運算,通過使用 checked 和 unchecked 關鍵字,可以檢查或忽略這樣的溢出。如果表達式是一個使用 checked 的常量表達式,則會在編譯時產生錯誤。
下面這個簡單的示例說明了這兩個運算符的用法
using System;
public class Class1
{
public static void Main(string[] args)
{
short a = 10000, b = 10000;
short d = unchecked((short)(10000*10000));
Console.WriteLine(d= + d);
short c = (short)(a*b);
Console.WriteLine(c= + c);
short e = checked((short)(a*b));
Console.WriteLine(e= + e);
}
}
在這段代碼中,unchecked 運算符避免了發生編譯時錯誤,否則,下面的語句會產生錯誤:
short d = unchecked((short)(10000*10000));
下一個表達式在默認情況下是不檢查的,因此值會悄悄溢出:
short c = (short)(a*b);
我們可以使用 checked 運算符來強制檢查該表達式是否會在運行時溢出:
short e = checked((short)(a*b));
當運行時,賦第一個值給 d & c 會以值 -7936 悄悄溢出,但是當試圖使用 checked() 以獲得 e 的乘積值時,程序會引發 System.OverflowException 異常。
注意:另外,通過使用命令行編譯器開關 (/checked) 或者直接在Visual Studio 中基於每個項目使用此開關,您還可以控制是否檢查代碼塊中的算術溢出。
is 運算符
此運算符確定左邊對象的類型是否與右邊指定的類型相匹配:
if (objReference is SomeClass) ...
在下面的示例中,CheckType() 方法打印一條消息,描述傳遞給它的參數的類型:
using System;
public class ShowTypes
{
public static void Main(string[] args)
{
CheckType (5);
CheckType (10f);
CheckType ("Hello");
}
private static void CheckType (object obj)
{
if (obj is int)
{
Console.WriteLine("Integer parameter");
}
else if (obj is float)
{
Console.WriteLine("Float parameter");
}
else if (obj is string)
{
Console.WriteLine("String parameter");
}
}
}
運行此程序,輸出如下:
Integer parameter
Float parameter
String parameter
sizeof 運算符
sizeof 運算符以指定值類型的字節數返回其大小,如下面的代碼所示:
using System;
public class Size
{
public static void Main()
{
unsafe
{
Console.WriteLine("The size of short is {0}.", sizeof(short));
Console.WriteLine("The size of int is {0}.", sizeof(int));
Console.WriteLine("The size of double is {0}.",sizeof(double));
}
}
}
注意,包含 sizeof 運算符的代碼放在一個不安全的塊中。這是因為 sizeof 運算符被認為是一個不安全的運算符(由於它直接訪問內存)。
typeof 和 GetType
typeof 運算符返回作為 System.Type 對象傳遞給它的類的類型。GetType() 方法是相關的,並且返回類或異常的運行時類型。typeof 和 GetType() 都可以與反射一起使用,以動態地查找關於對象的信息,如下面的示例所示:
using System; using System.Reflection; public class Customer { string name; public string Name { set { name = value; } get { return name; } } } public class TypeTest { public static void Main() { Type typeObj = typeof(Customer); Console.WriteLine("The Class name is {0}", typeObj.FullName); // Or use the GetType() method: //Customer obj = new Customer(); //Type typeObj = obj.GetType(); Console.WriteLine("\nThe Class Members\n=================\n "); MemberInfo[] class_members = typeObj.GetMembers(); foreach (MemberInfo members in class_members) { Console.WriteLine(members.ToString()); } Console.WriteLine("\nThe Class Methods\n=================\n"); MethodInfo[] class_methods = typeObj.GetMethods(); foreach (MethodInfo methods in class_methods) { Console.WriteLine(methods.ToString()); } } }
運行此程序,輸出如下:
The Class name is Customer
The Class Members
=================
Int32 GetHashCode()
Boolean Equals(System.Object)
System.String ToString()
Void set_Name(System.String)
System.String get_Name()
System.Type GetType()
Void .ctor()
System.String Name
The Class Methods
=================
Int32 GetHashCode()
Boolean Equals(System.Object)
System.String ToString()
Void set_Name(System.String)
System.String get_Name()
System.Type GetType()
這為我們顯示了從 System.Object 繼承的所有類的成員,並且還展示了一種方法,C# 在內部將 get 和 set 屬性 accessors 表示為 get_xxx() 和 set_xxx() 方法。
在下一個示例中,我們使用 GetType() 在運行時查找表達式的類型:
using System;
public class TypeTest
{
public static void Main()
{
int radius = 8;
Console.WriteLine("Calculated area is = {0}",
radius * radius * System.Math.PI);
Console.WriteLine("The result is of type {0}",
(radius * radius * System.Math.PI).GetType());
}
}
此程序的輸出告訴我們,結果是 System.Double 類型,選擇它是因為System.Math.PI 是這種類型。
Calculated area is = 201.061929829747
The result is of type System.Double
流程控制
在這兩種語言中,流程控制語句是非常相似的,但是這一部分也會討論它們的一些細微差別。
分支語句
分支語句根據特定的條件改變運行時程序執行的流程。
if、else 和 else if
這些在兩種語言中是一樣的。
switch 語句
在兩種語言中,switch 語句都提供條件多分支操作。但是有點不同的是,Java 允許您“越過”一個 case 並執行下一個 case,除非您在 case 的末尾使用了 break 語句。然而,C# 需要在每個 case 的末尾都使用 break 或 goto 語句,如果兩者都不存在,則編譯器會產生下列錯誤:
Control cannot fall through from one case label to another.
不過請注意,在沒有指定要執行的代碼的地方,當 case 匹配時,控制會越過隨後的 case。當在 switch 語句中使用 goto 時,我們只能跳至同一 switch 中的另一個 case 塊。如果我們想要跳至 default case,我們可以使用“goto default;”,否則,我們需要使用“goto case cond;”,其中 cond 是我們希望跳至的 case 的匹配條件。Java 的 switch 語句的另一個不同之處在於,在 Java 中,我們只能對整數類型使用 switch 語句,而 C# 允許我們對字符串變量使用 switch 語句。
例如,下面的程序在 C# 中是合法的,但在 Java 中卻是不合法的:
switch (args[0])
{
case "copy":
...
break;
case "move":
...
goto case "delete";
break;
case "del":
case "remove":
case "delete":
...
break;
default:
...
break;
}
goto 的返回
在 Java 中,goto 是一個沒有實現的保留關鍵字。然而,我們可以使用帶有 break 或 continue 標簽的語句來達到與 goto 相似的目的。
C# 允許 goto 語句跳至帶有標簽的語句。不過請注意,為了跳至一個特定的標簽,goto 語句必須在該標簽的范圍內。換句話說,goto 不可用來跳進一個語句塊(不過,它可以跳出一個語句塊)、跳出一個類,或退出 try...catch 語句中的 finally 塊。不過請注意,在大多數情況下,我們都不鼓勵您使用 goto,因為它違反了面向對象編程的良好實踐。
循環語句
循環語句重復指定的代碼塊,直到滿足給定的條件為止。
for 循環
在兩種語言中,for 循環的語法和操作均相同:
for (initialization; condition; expression)
statement;
foreach 循環
C# 引入了一種新的循環類型,稱為 foreach 循環(類似於 Visual Basic 的 For Each)。foreach 循環允許遍歷支持 IEnumerable 接口的容器類中的每一項(例如:數組)。下面的代碼說明了如何使用 foreach 語句來輸出一個數組的內容:
public static void Main()
{
int[] arr1= new int[] {1,2,3,4,5,6};
foreach ( int i in arr1)
{
Console.WriteLine("Value is {0}", i);
}
}
在 部分,我們將更詳細地介紹 C# 中的數組。
while 和 do...while 循環
在兩種語言中,while 和 do...while 語句的語法和操作均相同:
while (condition)
{
//statements
}
As usual, don't forget the trailing ; in do...while loops:
do
{
//statements
}
while(condition);
類基礎訪問修飾符
C# 中的修飾符與 Java 大致相同,我們將在這一部分介紹其中的一些細微差別。每個類成員或類本身都可以用訪問修飾符進行聲明,以定義許可訪問的范圍。沒有在其他類中聲明的類只能指定 public 或 internal 修飾符,而嵌套的類(如其他的類成員)可以指定下面五個修飾符中的任何一個:
•
public — 對所有類可見
•
protected —僅從派生類中可見
•
private — 僅在給定的類中可見
•
internal — 僅在相同的程序集中可見
•
protected internal — 僅對當前的程序集或從包含類中派生的類型可見
public、protected 和 private 修飾符
public 修飾符使得可以從類內外的任何地方訪問成員。protected 修飾符表示訪問僅限於包含類或從它派生的類。private 修飾符意味著只可能從包含類型中進行訪問。
internal 修飾符
internal 項只可以在當前的程序集中進行訪問。.NET 中的程序集大致等同於 Java 的 JAR 文件,它表示可以從中構造其他程序的生成塊。
protected internal 修飾符
protected internal 項僅對當前程序集或從包含類派生的類型可見。在 C# 中,默認訪問修飾符是 private,而 Java 的默認訪問修飾符是包范圍。
sealed 修飾符
在其類聲明中帶有 sealed 修飾符的類可以認為是與抽象類完全相反的類 — 它不能被繼承。我們可以將一個類標記為 sealed,以防止其他類重寫它的功能。自然地,sealed 類不能是抽象的。同時還需要注意,該結構是隱式密封的;因此,它們不能被繼承。sealed 修飾符相當於在 Java 中用 final 關鍵字標記類。
readonly 修飾符
要在 C# 中定義常量,我們可以使用 const 或 readonly 修飾符替換 Java 的 final 關鍵字。在 C# 中,這兩個修飾符之間的區別在於,const 項是在編譯時處理的,而 readonly 字段是在運行時設置的。這可以允許我們修改用於在運行時確定 readonly 字段值的表達式。
這意味著給 readonly 字段的賦值可以出現在類構造函數及聲明中。例如,下面的類聲明了一個名為 IntegerVariable 的 readonly 變量,它是在類構造函數中初始化的:
using System;
public class ReadOnlyClass
{
private readonly int IntegerConstant;
public ReadOnlyClass ()
{
IntegerConstant = 5;
}
// We get a compile time error if we try to set the value of the readonly
// class variable outside of the constructor
public int IntMember
{
set
{
IntegerConstant = value;
}
get
{
return IntegerConstant;
}
}
public static void Main(string[] args)
{
ReadOnlyClass obj= new ReadOnlyClass();
// We cannot perform this operation on a readonly field
obj.IntMember = 100;
Console.WriteLine("Value of IntegerConstant field is {0}",
obj.IntMember);
}
}
注意,如果 readonly 修飾符應用於靜態字段,它就應該在該靜態字段中進行初始化。
類的構造函數。
Main() 方法
每個 C# 應用程序都必須http://msdn.microsoft.com/vstudio/java/gettingstarted/csharpforjava/#Arrays in C#只能包含一個 Main() 方法,Main() 方法指定程序從何處開始執行。注意,在 C# 中,Main() 用大寫字母開頭,而 Java 使用小寫的 main()。
Main() 只能返回 int 或 void,並且有一個可選的字符串數組參數來表示命令行參數:
static int Main (string[] args)
{
...
return 0;
}
字符串數組參數可以包含任何傳入的命令行參數,這與 Java 中完全一樣。因此,args[0] 指定第一個命令行參數,而 args[1] 表示第二個參數,等等。與 C++ 不同的是,args 數組不包含 EXE 文件的名稱。
其他方法
當將參數傳遞給方法時,它們可能通過值傳遞,也可能通過引用傳遞。值參數只提取任何變量的值以在方法中使用,因而調用代碼中的變量值不受方法中對這些參數所執行的操作的影響。
而引用型參數指向在調用代碼中聲明的變量,因而在通過引用傳遞時,方法將修改該變量的內容。
通過引用傳遞
在 Java 和 C# 中,引用對象的方法參數總是通過引用傳遞,而基本數據類型參數則通過值傳遞。
在 C# 中,所有參數在默認情況下都是通過值傳遞的。要通過引用進行傳遞,我們需要指定關鍵字 ref 或 out 中的一個。這兩個關鍵字的不同之處在於參數的初始化。ref 參數必須在使用前進行初始化,而 out 參數無需在傳遞前進行顯式初始化,並且任何先前的值都會被忽略。
請注意,當方法將引用類型作為參數使用時,引用本身是通過值傳遞的。然而,引用仍然指向內存中的同一對象,因此對對象的屬性所做的任何改變在方法退出之後將保持不變。但是,因為引用本身是通過值傳遞的,所以在方法內它應該改為指向一個不同的對象甚至一個新對象,而一旦方法執行完畢,引用就會恢復為指向原來的對象,即使原來的對象是未賦值的也同樣如此。
ref 關鍵字
當我們希望調用的方法永久性地改變用作參數的變量的值時,我們可以在參數中指定此關鍵字。發生傳遞的不是在調用中使用的變量值,而是對變量本身的引用。然後,方法作用於引用,這樣,在方法執行過程中對參數所做的更改就保存到用作方法的參數的原始變量中。
下面的代碼用 Add() 方法對此進行了說明,其中,第二個 int 參數是使用 ref 關鍵字通過引用傳遞的:
using System;
public class RefClass
{
public static void Main(string[] args)
{
int total = 20;
Console.WriteLine("Original value of 'total': {0}", total);
// Call the Add method
Add (10, ref total);
Console.WriteLine("Value after Add() call: {0}", total);
}
public static void Add (int i, ref int result)
{
result += i;
}
}
這個簡單示例的輸出結果表明,對 result 參數所做的改變反映在 Add() 調用所用的變量 total 中:
Original value of 'total': 20
Value after Add() call: 30
這是因為 result 參數引用了調用代碼中的 total 變量所占用的實際內存位置。請注意,類的屬性不是一個變量,並且不能直接用作 ref 類型的參數。
注意,在調用方法時,ref 關鍵字必須在參數之前,在方法聲明中同樣如此。
out 關鍵字
out 關鍵字的作用與 ref 關鍵字非常相似,並且對使用 out 關鍵字聲明的參數所做的修改在方法外是可見的。它與 ref 有兩點不同:首先,out 參數的任何初始值在方法中都是忽略的;其次,out 參數必須在方法中賦值:
using System;
public class OutClass
{
public static void Main(string[] args)
{
int total = 20;
Console.WriteLine("Original value of 'total': {0}", total);
Add (33, 77, out total);
Console.WriteLine("Value after Add() call: {0}", total);
}
public static void Add (int i, int j, out int result)
{
// The following line would cause a compile error
// Console.WriteLine("Initial value inside method: {0}", result);
result = i + j;
}
}
在該示例中,Add() 方法的第三個參數是用 out 關鍵字聲明的,並且對該方法的調用還需要將 out 關鍵字用於該參數。輸出結果為:
Original value of 'total': 20Value after Add() call: 110
因此,總的來說,當您希望方法修改現有的變量時,可以使用 ref 關鍵字,而使用 out 關鍵字來返回在該方法中產生的值。當方法為調用代碼產生多個結果值時,通常一起使用 out 關鍵字與該方法的返回值。
使用不確定數目的參數
通過在聲明方法時指定 params 關鍵字,C# 允許我們發送可變數目的參數。參數列表也可以包含普通參數,但是需要注意,用 params 關鍵字聲明的參數必須放在最後。它采用可變長度數組的形式,並且每個方法只有一個 params 參數。
當編譯器嘗試解析一個方法調用時,它查找其參數列表與調用的方法相匹配的方法。如果找不到可以與參數列表匹配的方法重載,但是存在與適當類型的 params 參數匹配的版本,則會調用該方法,並將額外的參數放在一個數組中。
下面的示例對此進行了演示:
using System;
public class ParamsClass
{
public static void Main(string[] args)
{
Average ("List One", 5,10,15);
Average ("List Two", 5,10,15,20,25,30);
}
public static void Average (string title, params int[] values)
{
int Sum = 0;
Console.Write("Average of {0}: ", title);
for (int i = 0; i < values.Length; i++)
{
Sum += values[i];
Console.Write(values[i] + ", ");
}
Console.WriteLine(": {0}", (float)Sum/values.Length);
}
}
在上面的示例中,用整型數組中的 params 參數聲明了方法 Average,讓我們使用任何數目的參數來調用它。輸出結果如下:
Average of List One: 5, 10, 15, : 10
Average of List Two: 5, 10, 15, 20, 25, 30, : 17.5
注意,如果我們希望允許不同類型的不確定參數,我們可以指定 Object 類型的 params 參數。
屬性
在 C# 中,屬性是類、struct,或接口的命名成員,它提供了一種簡潔的途徑,可以通過所謂的 get 和 set 訪問器方法訪問私有字段。
下面的代碼片斷為類 Animal 聲明了一個名為 Species 的屬性,它抽象了對名為 name 的私有變量的抽象訪問:
public class Animal
{
private string name;
public string Species
{
get
{
return name;
}
set
{
name = value;
}
}
}
通常,屬性與它訪問的內部成員有相同的名稱,但是屬性以大寫字母開頭(例如上面的示例中的 Name),或者內部成員帶有“_”前綴。同時還需要注意 set 訪問器中所用的名為 value 的隱式參數 — 這種參數具有基礎成員變量類型。
實際上,訪問器在內部表示成 get_X() 和 set_X() 方法,從而與 .NET 語言保持兼容,因為 .NET 語言並不支持訪問器(如本文前面的 typeOf 和 GetType() 部分中的屏幕截圖所示)。一旦定義好屬性,就可以非常容易地獲取或設置它的值:
Animal animal = new Animal()
// Set the property
animal.Species = "Lion";
// Get the property value
string str = animal.Species;
如果屬性只有 get 訪問器,它就是一個只讀屬性。如果它只有 set 訪問器,它就是一個只寫屬性。如果兩者都有,則它是一個可讀寫屬性。
結構
C# 支持 struct 關鍵字,它是源於 C 的另一個項,但是不可用於 Java。可以將 struct 看作是一個輕量級類。它可以采用與類大致相同的方式包含構造函數、常量、字段、方法、屬性、索引器、運算符和嵌套類型。structs 不同於類,因為它們不能是抽象的,並且不支持實現繼承。結構與類還有一點重要的不同,結構是值類型的,而類是引用類型的。在構造函數的工作方式上,結構也有所不同。特別是,編譯器總是提供默認的不帶參數的構造函數,並且不允許您替換它。
在下面的示例中,我們使用 new 關鍵字並且通過初始化實例的成員初始化了一個 struct:
using System;
public struct CustomerStruct
{
public int ID;
public string name;
public CustomerStruct(int customerID, string customerName)
{
ID = customerID;
name = customerName;
}
}
class TestClass
{
public static void Main(string[] args)
{
// Declare a CustomerStruct using the default constructor
CustomerStruct customer = new CustomerStruct();
Console.WriteLine("Struct values before initialization");
Console.WriteLine("ID = {0}, Name = {1}", customer.ID,
customer.name);
customer.ID = 100;
customer.name = "Robert";
Console.WriteLine("Struct values after initialization");
Console.WriteLine("ID = {0}, Name = {1}", customer.ID,
customer.name);
}
}
當我們編譯並運行上面的代碼時,它的輸出顯示,該結構的變量在默認情況下是已初始化的。int 變量初始化為 0,而字符串變量初始化為空字符串:
初始化前的 struct 變量
ID = 0, Name =
初始化後的 truct 值
ID = 100, Name = Robert
注意,當我們使用另一種表示法(CustomerStruct customer)聲明 customer 時,它的成員變量將不被初始化,因此,如果試圖在為它們賦值前使用它們,將會產生編譯時錯誤。
C# 中的數組
數組是具有相同數據類型的項的有序集合,通過數組名以及所期望的項相對於數組開始位置的偏移量可以訪問數組。與 Java 相比,在 C# 中聲明和使用數組的方式有一些重要的不同,我將在這一部分中對此進行介紹。
一維數組
一維數組以線性方式存儲了固定數目的項,它僅僅需要一個索引值就可以確定任何一項。
在 C# 中,數組聲明中的方括號必須緊跟數據類型,而不可以像在 Java 中一樣出現在變量名的後面。因此,可以使用下面的語法來聲明整數類型的數組:
int[] MyArray;
而下面的聲明在 C# 中是無效的:
int MyArray[];
一旦聲明了數組,就可以使用新的關鍵字來設置它的大小,這與 Java 中是完全一樣的:
int[] MyArray; // declares the array reference
MyArray = new int[5]; // creates a 5 element integer array
然後,我們就可以使用與 Java 完全相同的語法來訪問一維數組中的元素,注意 C# 數組的索引也是從零開始的:
MyArray [4] // accesses the last element in the array
初始化
可以使用與 Java 相同的語法在創建時對數組元素進行初始化:
MyArray = new int[5] {1, 2, 3, 4, 5};
與 Java 不同,初始化器的數目必須與數組大小完全匹配。 我們可以利用這一特性在一行中聲明和初始化 C# 數組:
int[] TaxRates = {0, 20, 23, 40, 50};
此語法創建了一個大小與初始化器的數目相等的數組。
程序循環中的初始化
C# 中初始化數組的另一種方法就是使用 foreach 循環。下面的循環將數組中的每個元素都設置為零:
int[] MyLittleArray = new int[5];
foreach (int i in MyLittleArray)
{
MyLittleArray[i] = 0;
}
交錯數組
C# 和 Java 都支持創建交錯或者說非矩形的數組,其中的每一行都包含不同數目的列。例如,下面的交錯數組的第一行有四項,而第二行有三項:
int[][] JaggedArray = new int[2][];
JaggedArray[0] = new int[4];
JaggedArray[1] = new int[3];
多維數組
C# 允許我們創建規則的多維數組,它可以看作是相同類型的值的矩陣。雖然 Java 和 C# 都支持交錯的數組,但是 C# 還支持多維數組或數組的數組。我們將馬上介紹交錯數組。
使用下面的語法,我們可以聲明一個多維矩形數組:
int[,] My2DIntArray;
float[,,,] My4DFloatArray;
其中,My2DintArray 是可以借此訪問每個元素的名稱。
注意, int[][] My2DintArray; 行在 C# 中有不同的含義,我們很快就會明白這一點。
一旦聲明了數組,我們就可以這樣給它分配內存:
int[,] My2DIntArray; // declares array reference
My2DIntArray = new int[5,4]; // allocates space for 5x4 integers
然後,可以使用下面的語法來訪問數組的元素:
My2DIntArray [4,3] = 906;
因為數組是從零開始的,所以這將第四行第五列中的元素(右下角)設置為 906。
初始化
通過下面的任何一種方法,都可以在一行中創建、設置和初始化多維數組:
int[,] intArray = { {1,2,3},
{4,5,6} };
int[,] intArray = new int [2,3] { {1,2,3},
{4,5,6} };
int[,] intArray = new int [,] { {1,2,3},
{4,5,6} };
程序循環中的初始化
數組中所有的元素都可以使用嵌套的循環進行初始化,如下所示:
int[,] intArray = new int[5,4];
foreach (int i in intArray)
{
foreach (int j in intArray[])
{
j = 0;
}
}
System.Array 類
在 .NET 中,數組是作為 System.Array 類的實例實現的。此類提供了幾個有用的方法,例如 Sort() 和 Reverse()。
下面的程序說明了使用這幾個方法是多麼的容易。首先,我們使用 Array 類的 Reverse() 方法來使數組的元素反向,然後,我們使用 Sort() 方法對它們進行排序:
using System; public class ArrayMethods { public static void Main() { // Create string array of size 5 string[] EmployeeNames = new string[5]; Console.WriteLine("Enter five employee names:"); // Read 5 employee names from user for(int i=0;i<5;i++) { EmployeeNames[i]= Console.ReadLine(); } // Print the array in original order Console.WriteLine("\n** Original Array **"); foreach(string EmployeeName in EmployeeNames) { Console.Write("{0} ", EmployeeName); } //print the array in reverse order. Console.WriteLine("\n\n** Values in Reverse Order **"); System.Array.Reverse(EmployeeNames); foreach(string EmployeeName in EmployeeNames) { Console.Write("{0} ", EmployeeName); } //print the array in sorted order. Console.WriteLine("\n\n** Values in Sorted Order **"); System.Array.Sort(EmployeeNames); foreach(string EmployeeName in EmployeeNames) { Console.Write("{0} ", EmployeeName); } } }
下面是此程序的一些典型輸出:
Enter five employee names:
Luca
Angie
Brian
Kent
Beatriz
** Original Array **
Luca Angie Brian Kent Beatriz
** Values in Reverse Order **
Beatriz Kent Brian Angie Luca
** Values in Sorted Order **
Angie Beatriz Brian Kent Luca
繼承和派生類
通過創建一個從現有類派生的新類,我們可以擴展現有類的功能。派生類繼承了基類的屬性,並且我們可以在需要時添加或重寫方法和屬性。
在 C# 中,繼承和接口實現都通過 : 運算符來定義,這等同於 Java 中的擴展和實現。注意,基類應該一直在類聲明的最左邊。
同 Java 一樣,C# 不支持多重繼承,這意味著類不能從多個類中繼承。然而,我們可以為此目的而采用與 Java 相同的方式使用接口,正如我們在下一部分中將看到的。
下面的代碼定義了一個名為 Point 的類,它有兩個私有成員變量 x 和 y,表示點的位置。這些變量可以分別通過名為 X 和 Y 的屬性來訪問:
public class Point
{
private int x, y;
public Point()
{
x = 0;
y = 0;
}
public int X
{
get
{
return x;
}
set
{
x = value;
}
}
public int Y
{
get
{
return y;
}
set
{
y = value;
}
}
}
我們將從 Point 類派生一個新類(比如說名為 ColorPoint),如下所示:
public class ColorPoint : Point
於是 ColorPoint 繼承了基類的所有字段和方法,我們可以根據需要向派生類中添加新的字段和方法來提供額外的特性。在這個示例中,我們添加了一個私有成員和一些訪問器來為點增加顏色:
using System.Drawing;
public class ColorPoint : Point
{
private Color screenColor;
public ColorPoint()
{
screenColor = Color.Red;
}
public Color ScreenColor
{
get
{
return screenColor;
}
set
{
screenColor = value;
}
}
}