前言
C#
是一種面向?qū)ο?、類型安全的語言。
?什么是面向?qū)ο?/p>
面向?qū)ο缶幊蹋∣OP)是如今多種編程語言所實(shí)現(xiàn)的一種編程范式,包括 Java、C++、C#。
面向?qū)ο缶幊虒⒁粋€(gè)系統(tǒng)抽象為許多對(duì)象的集合,每一個(gè)對(duì)象代表了這個(gè)系統(tǒng)的特定方面。對(duì)象包括函數(shù)(方法)和數(shù)據(jù)。一個(gè)對(duì)象可以向其他部分的代碼提供一個(gè)公共接口,而其他部分的代碼可以通過公共接口執(zhí)行該對(duì)象的特定操作,系統(tǒng)的其他部分不需要關(guān)心對(duì)象內(nèi)部是如何完成任務(wù)的,這樣保持了對(duì)象自己內(nèi)部狀態(tài)的私有性。
面向?qū)ο蠛兔嫦蜻^程的區(qū)別:
面向?qū)ο螅河镁€性的思維。與面向過程相輔相成。在開發(fā)過程中,宏觀上,用面向?qū)ο髞戆盐帐挛镩g復(fù)雜的關(guān)系,分析系統(tǒng)。微觀上,仍然使用面向過程。
面向過程:是一種是事件為中心的編程思想。就是分析出解決問題所需的步驟,然后用函數(shù)把這寫步驟實(shí)現(xiàn),并按順序調(diào)用。
簡單來說:用面向過程的方法寫出來的程序是一份蛋炒飯,而用面向?qū)ο髮懗鰜淼某绦蚴且环萆w澆飯。所謂蓋澆飯,就是在米飯上面澆上一份蓋菜,你喜歡什么菜,你就澆上什么菜。
這個(gè)比喻還是比較貼切的。
?為什么使用面向?qū)ο缶幊?/p>
面向?qū)ο缶幊蹋梢宰尵幊谈忧逦?,把程序中的功能進(jìn)行模塊化劃分,每個(gè)模塊提供特定的功能,同時(shí)每個(gè)模塊都是孤立的,這種模塊化編程提供了非常大的多樣性,大大增加了重用代碼的機(jī)會(huì),而且各模塊不用關(guān)心對(duì)象內(nèi)部是如何完成的,可以保持內(nèi)部的私有性。簡單來說面向?qū)ο缶幊叹褪墙Y(jié)構(gòu)化編程,對(duì)程序中的變量結(jié)構(gòu)劃分,讓編程更清晰。
準(zhǔn)確地說,本文所提及到的特性是一種特別的面向?qū)ο缶幊谭绞剑?strong>基于類的面向?qū)ο缶幊?/strong>(class-based OOP)。當(dāng)人們談?wù)撁嫦驅(qū)ο缶幊虝r(shí),通常來說是指基于類的面向?qū)ο缶幊獭?/p>
類 - 實(shí)際上是創(chuàng)建對(duì)象的模板。當(dāng)你定義一個(gè)類時(shí),你就定義了一個(gè)數(shù)據(jù)類型的藍(lán)圖。這實(shí)際上并沒有定義任何的數(shù)據(jù),但它定義了類的名稱,這意味著什么,這意味著類的對(duì)象由什么組成及在這個(gè)對(duì)象上可執(zhí)行什么操作。對(duì)象是類的實(shí)例。構(gòu)成類的方法和變量稱為類的成員。
類的定義和使用
類中的數(shù)據(jù)和函數(shù)稱為類的成員:
- 數(shù)據(jù)成員
- 數(shù)據(jù)成員是包含類的數(shù)據(jù) - 字段,常量和事件的成員。
- 函數(shù)成員
- 函數(shù)成員提供了操作類中數(shù)據(jù)的某些功能 - 方法,屬性,構(gòu)造器(構(gòu)造方法)和終結(jié)器(析構(gòu)方法),運(yùn)算符,和索引器
拿控制臺(tái)程序?yàn)槔?dāng)我們創(chuàng)建一個(gè)空的控制臺(tái)項(xiàng)目,在Main()
函數(shù)里編程的時(shí)候就是在Program
類里面操作的:
而且,我們可以發(fā)現(xiàn),Program
類和保存它的文件的文件名其實(shí)是一樣的Program.cs
,一般我們習(xí)慣一個(gè)文件一個(gè)類,類名和文件名一致。當(dāng)然了,這不是說一個(gè)文件只能寫一個(gè)類,一個(gè)文件是可以包含多個(gè)類的。
新建一個(gè)Customer
類來表示商店中購物的顧客:
class Customer
{
public string name;
public string address;
public int age;
public string createTime; // 加入會(huì)員的時(shí)間
public void Show()
{
Console.WriteLine("名字:" + name);
Console.WriteLine("地址:" + address);
Console.WriteLine("年齡:" + age);
Console.WriteLine("創(chuàng)建時(shí)間:" + createTime);
}
}
Customer
類里有四個(gè)公有字段和一個(gè)共有方法Show()
來輸出顧客信息。
創(chuàng)建Customer
類的對(duì)象:
static void Main(string[] args)
{
Customer customer = new Customer();
customer.name = "Test";
customer.address = "Test01";
customer.age = 24;
customer.createTime = "2023-02-27";
customer.Show();
Console.ReadKey();
}
通過類創(chuàng)建的變量被稱之為對(duì)象,這個(gè)過程我們叫他實(shí)例化。所有對(duì)象在使用之前必須實(shí)例化,僅僅聲明一個(gè)對(duì)象變量或者賦值為null
都是不行的。到現(xiàn)在看來,其實(shí)簡單的類在定義和使用起來跟結(jié)構(gòu)體是差不多的,只不過結(jié)構(gòu)體在創(chuàng)建的時(shí)候沒有實(shí)例化的過程,因?yàn)榻Y(jié)構(gòu)體是值類型的數(shù)據(jù)結(jié)構(gòu),而類是引用類型。
小小練習(xí)
推薦大家開發(fā)過程中,盡量一個(gè)文件里面一個(gè)類,當(dāng)然一個(gè)文件可以放多個(gè)類,但管理起來不方便,一個(gè)類一個(gè)文件管理起來方便,如果程序很小,怎么寫都無所謂,如果程序大或團(tuán)隊(duì)合作,最好一個(gè)類一個(gè)文件。
而且一個(gè)類定義也可以在多個(gè)文件中哦 -
partial className
定義一個(gè)車輛
Vehicle
類,具有Run
、Stop
等方法,具有Speed
( 速度 ) 、MaxSpeed
( 最大速度 ) 、Weight
( 重量 )等(也叫做字段)。使用這個(gè)類聲明一個(gè)變量(對(duì)象)。
static void Main(string[] args)
{
Vehicle vehicle = new Vehicle();
vehicle.brand = "BMW X5";
vehicle.speed = 90;
vehicle.maxSpeed = 215;
vehicle.weight = 32;
vehicle.Run();
vehicle.Stop();
Console.ReadKey();
}
class Vehicle
{
// 字段
public string brand;
public int speed;
public int maxSpeed;
public float weight;
// 方法
public void Run()
{
Console.WriteLine("Run!");
}
public void Stop()
{
Console.WriteLine("Stop!");
}
}
定義一個(gè)向量
Vector
類,里面有x,y,z
三個(gè)字段,有取得長度的方法,有設(shè)置屬性Set
的方法使用這個(gè)類聲明一個(gè)變量(對(duì)象)。
class Vector3
{
// 字段
private double x;
private double y;
private double z;
// 屬性【X】 - SetX為一個(gè)普通方法
public void SetX(double temp)
{
x = temp;
}
public void SetY(double temp)
{
y = temp;
}
public void SetZ(double temp)
{
z = temp;
}
// 方法
public double GetLength()
{
return Math.Sqrt(x * x + y * y + z * z);
}
}
屬性 - 是類的一種成員,它提供靈活的機(jī)制來讀取、寫入或計(jì)算私有字段的值。 屬性可用作公共數(shù)據(jù)成員,但它們是稱為“訪問器”的特殊方法。 此功能使得可以輕松訪問數(shù)據(jù),還有助于提高方法的安全性和靈活性。
這里先不詳細(xì)說,后續(xù)章節(jié)再展開。Vector3
類里面的Set*
屬性是用來給x,y,z
賦值的,可以看到與之前的簡單類不同的是,Vector3
類里的字段是private
也就是私有的,這意味著在類的外部是沒有辦法訪問這寫字段的,它只在類自己內(nèi)部是大家都知道的,到外面就不行了。
這里一開始寫錯(cuò)了,類Vector3
中的SetX
、SetY
和 SetZ
方法是普通的方法,而不是屬性。它們僅僅是修改和訪問實(shí)例中私有字段的方法。它們需要一個(gè)參數(shù)才能設(shè)置相應(yīng)的字段值,而屬性是通過訪問器方法來設(shè)置或獲取字段的值,并且不需要額外的參數(shù)。
public 和 private 訪問修飾符
- 訪問修飾符(C# 編程指南)
-
public
修飾的數(shù)據(jù)成員和成員函數(shù)是公開的,所有的用戶都可以進(jìn)行調(diào)用。 -
private
修飾詞修飾的成員變量以及成員方法只供本類使用,也就是私有的,其他用戶是不可調(diào)用的。
public
和private
這兩個(gè)修飾符其實(shí)從字面意思就可以理解,沒什么不好理解的,前者修飾的字段大家可以隨意操作,千刀萬剮只要你樂意,而后者修飾的字段就不能任你宰割了,你只能通過Get
、Set
進(jìn)行一系列的訪問或者修改。
舉個(gè)例子,生活中每個(gè)人都有名字、性別,同時(shí)也有自己的銀行卡密碼,當(dāng)別人跟你打交道的時(shí)候,他一般會(huì)先得知你的名字,性別,這些告訴他是無可厚非的,但是當(dāng)他想知道你的銀行卡密碼的時(shí)候就不太合適了對(duì)吧。假設(shè)我們有一個(gè)類Person
,我們就可以設(shè)置Name,Sex
等字段為公有的public
,大家都可以知道,但是銀行卡密碼就不行,它得是私有的,只有你自己知道。但是加入你去銀行ATM機(jī)取錢,它就得知道你的銀行卡密碼才能讓你取錢對(duì)吧,前面我們已經(jīng)了密碼是私有的,外部是沒辦法訪問的,那該怎么辦呢,這個(gè)時(shí)候就用到屬性了。我們用Get
獲取密碼,用Set
修改密碼。
放在代碼里面:
static void Main(string[] args)
{
Vector3 vector = new Vector3();
vector.w = 2;
vector.SetX(1);
Console.WriteLine(vector.GetX());
Console.ReadKey();
}
class Vector3
{
// 字段
private double x;
public double w;
// 屬性
public void SetX(double temp)
{
x = temp;
}
// ......
public double GetX()
{
return x;
}
}
w
字段在類外部可以直接操作,x
只能通過Get
、Set
來操作。
日常開發(fā)推薦不要把字段設(shè)置為共有的,至少要有點(diǎn)訪問限制,當(dāng)然了除了這兩個(gè)修飾符,還有其他的,比如internal
、protect
等等,以后的文章可能會(huì)專門來寫(?)。
使用private
修飾符除了多了一堆屬性(訪問器)有什么便利嗎?顯然得有,public
的字段你在設(shè)置的時(shí)候說啥就啥,即使它給到的內(nèi)容可能不適合這個(gè)字段,在后者,我們可以在屬性里設(shè)置一些限制或者是操作。比如,Vector3
類的x
字段顯然長度是不會(huì)出現(xiàn)負(fù)值的,這時(shí)候我們就可以在SetX
里面做些限制:
public void SetX(double temp)
{
if (temp<0)
{
Console.WriteLine("數(shù)據(jù)不合法。");
}
x = temp;
}
對(duì)于不想讓外界訪問的信息我們可以不提供Get
屬性以起到保護(hù)作用。
構(gòu)造函數(shù)
構(gòu)造函數(shù)(C# 編程指南)
構(gòu)造函數(shù) - 也被稱為“構(gòu)造器”,是執(zhí)行類或結(jié)構(gòu)體的初始化代碼。每當(dāng)我們創(chuàng)建類或者結(jié)構(gòu)體的實(shí)例的時(shí)候,就會(huì)調(diào)用它的構(gòu)造函數(shù)。大家可能會(huì)疑惑,我們上面創(chuàng)建的類里面也沒說這個(gè)構(gòu)造函數(shù)這個(gè)東東啊,那是因?yàn)槿绻粋€(gè)類沒有顯式實(shí)例構(gòu)造函數(shù),C#
將提供可用于實(shí)現(xiàn)實(shí)例化該類實(shí)例的無參構(gòu)造函數(shù)(隱式),比如:
public class Person
{
public int age;
public string name = "unknown";
}
class Example
{
static void Main()
{
var person = new Person();
Console.WriteLine($"Name: {person.name}, Age: {person.age}");
// Output: Name: unknown, Age: 0
}
}
默認(rèn)構(gòu)造函數(shù)根據(jù)相應(yīng)的初始值設(shè)定項(xiàng)初始化實(shí)例字段和屬性。 如果字段或?qū)傩詻]有初始值設(shè)定項(xiàng),其值將設(shè)置為字段或?qū)傩灶愋偷哪J(rèn)值。 如果在某個(gè)類中聲明至少一個(gè)實(shí)例構(gòu)造函數(shù),則 C# 不提供無參數(shù)構(gòu)造函數(shù)。
回到開頭,構(gòu)造函數(shù)有什么作用呢?
我們構(gòu)造對(duì)象的時(shí)候,對(duì)象的初始化過程是自動(dòng)完成的,但是在初始化對(duì)象的過程中有的時(shí)候需要做一些額外的工作,比如初始化對(duì)象存儲(chǔ)的數(shù)據(jù),構(gòu)造函數(shù)就是用于初始化數(shù)據(jù)的函數(shù)。 使用構(gòu)造函數(shù),開發(fā)人員能夠設(shè)置默認(rèn)值、限制實(shí)例化,并編寫靈活易讀的代碼。
構(gòu)造函數(shù)是一種方法。
構(gòu)造函數(shù)的定義和方法的定義類似,區(qū)別僅在于構(gòu)造函數(shù)的函數(shù)名只能和封裝它的類型相同。聲明基本的構(gòu)造函數(shù)的語法就是聲明一個(gè)和所在類同名的方法,但是該方法沒有返回類型。
拿之前的Customer
類為例,我們來給他寫一個(gè)簡單的構(gòu)造函數(shù):
static void Main(string[] args)
{
Customer customer = new Customer();
// Output :我一個(gè)構(gòu)造函數(shù)。
Console.ReadKey();
}
class Customer
{
public string name;
public string address;
public int age;
public string createTime; // 加入會(huì)員的時(shí)間
public Customer()
{
Console.WriteLine("我一個(gè)構(gòu)造函數(shù)。");
}
public void Show()
{
Console.WriteLine("名字:" + name);
Console.WriteLine("地址:" + address);
Console.WriteLine("年齡:" + age);
Console.WriteLine("創(chuàng)建時(shí)間:" + createTime);
}
}
當(dāng)我們創(chuàng)建Customer
類的實(shí)例的時(shí)候就會(huì)調(diào)用我們寫無參的構(gòu)造函數(shù),雖然這個(gè)目前這個(gè)函數(shù)是沒什么實(shí)際意義的,我們一般使用構(gòu)造函數(shù)中實(shí)現(xiàn)數(shù)據(jù)初始化,比如我們來實(shí)現(xiàn)對(duì)顧客信息的初始化:
static void Main(string[] args)
{
Customer customer = new Customer();
Customer customer2 = new Customer("光頭強(qiáng)", "狗熊嶺", 30, "2305507");
customer2.Show();
// Output:
// 我一個(gè)構(gòu)造函數(shù)。
// 名字:光頭強(qiáng)
// 地址:狗熊嶺
// 年齡:30
// 創(chuàng)建時(shí)間:2305507
Console.ReadKey();
}
class Customer
{
public string name;
public string address;
public int age;
public string createTime; // 加入會(huì)員的時(shí)間
public Customer()
{
Console.WriteLine("我一個(gè)構(gòu)造函數(shù)。");
}
public Customer(string arg1, string arg2, int arg3, string arg4)
{
name = arg1;
address = arg2;
age = arg3;
createTime = arg4;
}
public void Show()
{
Console.WriteLine("名字:" + name);
Console.WriteLine("地址:" + address);
Console.WriteLine("年齡:" + age);
Console.WriteLine("創(chuàng)建時(shí)間:" + createTime);
}
}
有參的構(gòu)造函數(shù)相當(dāng)于無參構(gòu)造函數(shù)的重載,在創(chuàng)建實(shí)例時(shí),運(yùn)行時(shí)會(huì)自動(dòng)匹配對(duì)應(yīng)的構(gòu)造函數(shù)。這是時(shí)候輸出的內(nèi)容里面”我是”我一個(gè)構(gòu)造函數(shù)“是在創(chuàng)建實(shí)例customer
的時(shí)候調(diào)用的無參構(gòu)造函數(shù),customer2
在創(chuàng)建的時(shí)候調(diào)用的時(shí)對(duì)應(yīng)四個(gè)參數(shù)的有參構(gòu)造函數(shù)。進(jìn)行有參構(gòu)造的實(shí)例時(shí)一定注意對(duì)應(yīng)的參數(shù)列表:類型、數(shù)量等必須一致,否則就不能成功創(chuàng)建實(shí)例。
當(dāng)我們注釋掉Customer
類里的無參構(gòu)造函數(shù)后,Customer customer = new Customer();
就會(huì)報(bào)錯(cuò),這就是我們上面所說的,如果在某個(gè)類中聲明至少一個(gè)實(shí)例構(gòu)造函數(shù),則 C# 不提供默認(rèn)的無參數(shù)構(gòu)造函數(shù)。
我們例子中的四個(gè)參數(shù)的構(gòu)造函數(shù)在使用起來是很不方便的,參數(shù)arg1
在我們創(chuàng)建實(shí)例的時(shí)候可能會(huì)混淆,不清楚哪個(gè)參數(shù)代表哪個(gè)字段,假入你現(xiàn)在使用的是Visual Studio 2022,你在創(chuàng)建類以后,IntelliSense
代碼感知工具可能會(huì)給你生成一個(gè)和類中字段匹配的構(gòu)造函數(shù):
public Customer(string name,string address,int age,string createTime)
{
this.name = name;
this.address = address;
this.age = age;
this.createTime = createTime;
}
你會(huì)發(fā)現(xiàn)這個(gè)構(gòu)造函數(shù)的參數(shù)和Customer
的字段是一樣的,類型、變量名都一樣,這個(gè)時(shí)候就需要用到this
關(guān)鍵字了,如果這個(gè)時(shí)候我們還寫成name = name;
就會(huì)出錯(cuò),雖然我們可能知道前面name
是字段,后面的是傳遞進(jìn)去的參數(shù),但是編譯器是不認(rèn)識(shí)的,咱們這樣寫完它的CPU就冒煙了,這是干啥呢,誰是誰啊。
簡單概述,后面會(huì)有章節(jié)展開說。this
關(guān)鍵字指代類的當(dāng)前實(shí)例,我們可以通過this
訪問類中字段來區(qū)分變量。
屬性
為了保護(hù)數(shù)據(jù)安全,類里面的字段我們一般都設(shè)置為私有的,之前的Vector3
類中我們是通過編寫Get
、Set
方法來訪問或者修改字段的數(shù)據(jù),這樣在實(shí)際開發(fā)中是很麻煩的,會(huì)降低我們的效率而且使用起來我們必須通過調(diào)用這兩個(gè)方法來實(shí)現(xiàn)對(duì)私有字段的操作:
static void Main(string[] args)
{
Customer customer = new Customer();
customer.SetAge(24);
Console.WriteLine(customer.GetAge());
// Output: 24
Console.ReadKey();
}
class Customer
{
public string name;
public string address;
public int age;
public string createTime;
public void SetAge(int age)
{
this.age = age;
}
public int GetAge()
{
return this.age; // 這里 this 可加可不加
}
public void Show()
{
Console.WriteLine("名字:" + name);
Console.WriteLine("地址:" + address);
Console.WriteLine("年齡:" + age);
Console.WriteLine("創(chuàng)建時(shí)間:" + createTime);
}
}
我們可以通過屬性來快捷實(shí)現(xiàn)對(duì)私有字段的訪問以及修改,通過get
、set
訪問器操作私有字段的值。
?什么是屬性呢
-
屬性是一種成員,它提供靈活的機(jī)制來讀取、寫入或計(jì)算私有字段的值。 屬性可用作公共數(shù)據(jù)成員,但它們是稱為“訪問器”的特殊方法。 此功能使得可以輕松訪問數(shù)據(jù),還有助于提高方法的安全性和靈活性。
-
屬性允許類公開獲取和設(shè)置值的公共方法,而隱藏實(shí)現(xiàn)或驗(yàn)證代碼。
-
屬性可以是讀-寫屬性(既有
get
訪問器又有set
訪問器)、只讀屬性(有get
訪問器,但沒有set
訪問器)或只寫訪問器(有set
訪問器,但沒有get
訪問器)。 只寫屬性很少出現(xiàn),常用于限制對(duì)敏感數(shù)據(jù)的訪問。 -
不需要自定義訪問器代碼的簡單屬性可以作為表達(dá)式主體定義或自動(dòng)實(shí)現(xiàn)的屬性來實(shí)現(xiàn)。
上面的SetAge
和GetAge
方法我們用屬性替換掉就是:
static void Main(string[] args)
{
Customer customer = new Customer();
customer.Age = 10;
Console.WriteLine(customer.Age);
// Output: 10
Console.ReadKey();
}
class Customer
{
private string name;
private string address;
private int age;
private string createTime;
// 屬性
public int Age
{
get
{
return this.age;
}
set // value 參數(shù)
{
this.age = value;
}
}
public void Show()
{
Console.WriteLine("名字:" + name);
Console.WriteLine("地址:" + address);
Console.WriteLine("年齡:" + age);
Console.WriteLine("創(chuàng)建時(shí)間:" + createTime);
}
}
屬性的時(shí)候就像訪問一個(gè)公有的字段一樣方便,我們?cè)诳梢韵袷且粋€(gè)普通的公有的數(shù)據(jù)成員一樣使用屬性。只不過我們通過屬性Age
進(jìn)行賦值的時(shí)候,在類的內(nèi)部會(huì)調(diào)用set
訪問器,這是我們給屬性Age
賦的值就會(huì)被當(dāng)作value
參數(shù)傳遞進(jìn)去,實(shí)現(xiàn)賦值;同理,我們?cè)谑褂脤傩?code>Age的時(shí)候也是通過get
訪問器來實(shí)現(xiàn)的。
上面屬性
Age
里的關(guān)鍵字可以不寫也沒問題的。
除了進(jìn)行簡單數(shù)據(jù)訪問和賦值,我們有一個(gè)實(shí)現(xiàn)屬性的基本模式: get
訪問器返回私有字段的值,set
訪問器在向私有字段賦值之前可能會(huì)執(zhí)行一些數(shù)據(jù)驗(yàn)證。 這兩個(gè)訪問器還可以在存儲(chǔ)或返回?cái)?shù)據(jù)之前對(duì)其執(zhí)行某些轉(zhuǎn)換或計(jì)算。
比如我們可以驗(yàn)證顧客的年齡不為負(fù)值:
static void Main(string[] args)
{
Customer customer = new Customer();
customer.Age = -10;
// 引發(fā) ArgumentOutOfRangeException 異常
Console.ReadKey();
}
class Customer
{
private string name;
private string address;
private int age;
private string createTime;
// 屬性
public int Age
{
get
{
return this.age;
}
set // value 參數(shù)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "The age must be greater than 0.");
}
this.age = value;
}
}
}
同時(shí)呢,我們一個(gè)定義訪問器的訪問權(quán)限,如果在Age
屬性的set
訪問器前面加上private
修飾符,那我們就沒辦法使用 customer.Age = -10;
來進(jìn)行賦值了,編譯器會(huì)告知錯(cuò)誤set
訪問器無法訪問。
此外,我們可以通過get
訪問器和 set
訪問器的有無來控制屬性是讀 - 寫、只讀、還是只寫,只寫屬性很少出現(xiàn),常用于限制對(duì)敏感數(shù)據(jù)的訪問。
class Customer
{
private string name;
private string address;
private int age;
private string createTime;
// 屬性
public int Age // 讀 - 寫
{
get
{
return this.age;
}
set // value 參數(shù)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "The age must be greater than 0.");
}
this.age = value;
}
}
public string Name // 只讀
{
get { return this.name; }
}
public string Address // 只寫
{
set { this.address = value; }
}
}
表達(dá)式屬性
從C# 6
開始,只讀屬性(就像之前的例子中那樣的屬性)可簡寫為表達(dá)式屬性。它使用雙箭頭替換了花括號(hào)、get訪問器和return關(guān)鍵字。
class Customer
{
private string name;
private string address;
private int age;
private string createTime;
public int Age => age; // 表達(dá)式屬性 只讀屬性
}
C# 7
進(jìn)一步允許在set
訪問器上使用表達(dá)式體:
class Customer
{
private string name;
private string address;
private int age;
private string createTime;
public int Age => age;
public string Name { get => name; set => name = value; }
public string Address{ set => address = value; }
}
自動(dòng)實(shí)現(xiàn)的屬性
當(dāng)屬性訪問器中不需要任何其他邏輯時(shí),自動(dòng)實(shí)現(xiàn)的屬性會(huì)使屬性聲明更加簡潔。
自動(dòng)實(shí)現(xiàn)的屬性是C# 3.0
引入的新特性,它可以讓我們?cè)诓伙@式定義字段和訪問器方法的情況下快速定義一個(gè)屬性。具體來說,一個(gè)屬性包含一個(gè)字段和兩個(gè)訪問器方法,其中get
和set
訪問器方法都是自動(dòng)實(shí)現(xiàn)的。
static void Main(string[] args)
{
Customer customer = new Customer();
customer.name = "光頭強(qiáng)";
customer.address = "狗熊嶺";
customer.age = 30;
customer.createTime = "2305507";
customer.Show();
// output:
// 名字:光頭強(qiáng)
// 地址:狗熊嶺
// 年齡:30
// 創(chuàng)建時(shí)間:2305507
Console.ReadKey();
}
class Customer
{
// 自動(dòng)實(shí)現(xiàn)的屬性
public string name { get; set; }
public string address { get; set; }
public int age { get; set; }
public string createTime { get; set; }
public void Show()
{
Console.WriteLine("名字:" + name);
Console.WriteLine("地址:" + address);
Console.WriteLine("年齡:" + age);
Console.WriteLine("創(chuàng)建時(shí)間:" + createTime);
}
}
屬性初始化器
C# 6
開始支持自動(dòng)屬性的初始化器。其寫法就像初始化字段一樣:
public int age { get; set; }=24;
上述寫法將``age`的值初始化為24。擁有初始化器的屬性可以為只讀屬性:
public string sex { get; } = "male";
就像只讀字段那樣,只讀自動(dòng)屬性只可以在類型的構(gòu)造器中賦值。這個(gè)功能適于創(chuàng)建不可變(只讀)的對(duì)象。
匿名類型
- 匿名類型
匿名類型提供了一種方便的方法,可用來將一組只讀屬性封裝到單個(gè)對(duì)象中,而無需首先顯式定義一個(gè)類型。 類型名由編譯器生成,并且不能在源代碼級(jí)使用。 每個(gè)屬性的類型由編譯器推斷,是一個(gè)由編譯器臨時(shí)創(chuàng)建來存儲(chǔ)一組值的簡單類。如果需要?jiǎng)?chuàng)建一個(gè)匿名類型,則可以使用new
關(guān)鍵字,后面加上對(duì)象初始化器,指定該類型包含的屬性和值。例如:
? var dude = new { Name = "Bob", Age = 23 };
編譯器將會(huì)把上述語句(大致)轉(zhuǎn)變?yōu)椋?/p>
internal class AnonymousGeneratedTypeName
{
private string name; // Actual field name is irrelevant
private int age; // Actual field name is irrelevant
public AnonymousGeneratedTypeName (string name, int age)
{
this.name = name; this.age = age;
}
public string Name { get { return name; } }
public int Age { get { return age; } }
// The Equals and GetHashCode methods are overridden (see Chapter 6).
// The ToString method is also overridden.
}
...
var dude = new AnonymousGeneratedTypeName ("Bob", 23);
匿名類型只能通過var
關(guān)鍵字來引用,因?yàn)樗]有一個(gè)名字。
堆、棧
程序在運(yùn)行時(shí),內(nèi)存一般從邏輯上分為兩大塊 - 堆、棧。
- 堆棧(Stack - 因?yàn)楹投岩黄鸾兄鴦e扭,所以簡稱為棧):棧是一種先進(jìn)后出(Last-In-First-Out,LIFO)的數(shù)據(jù)結(jié)構(gòu)。當(dāng)你聲明一個(gè)變量時(shí),它會(huì)自動(dòng)地被分配到棧內(nèi)存中,并且它的作用域僅限于當(dāng)前代碼塊。在方法中聲明的局部變量就是放在棧中的。棧的好處是,由于它的操作特性,棧的訪問非???,它也沒有垃圾回收的問題。??臻g比較小,但是讀取速度快。
- 堆(Heap):堆是一種動(dòng)態(tài)分配內(nèi)存的數(shù)據(jù)結(jié)構(gòu)。堆內(nèi)存的大小不受限制,而且程序員可以控制它的生命周期,也就是說,在堆上分配的內(nèi)存需要手動(dòng)釋放。堆空間比較大,但是讀取速度慢。
堆和棧就相當(dāng)于倉庫和商店,倉庫放的東西多,但是當(dāng)我們需要里面的東西時(shí)需要去里面自行查找然后取出來,后者雖然存放的東西沒有前者多,但是好在隨拿隨取,方便快捷。
棧
棧是一種先進(jìn)后出(Last-In-First-Out,LIFO)的數(shù)據(jù)結(jié)構(gòu)。本質(zhì)上講堆棧也是一種線性結(jié)構(gòu),符合線性結(jié)構(gòu)的基本特點(diǎn):即每個(gè)節(jié)點(diǎn)有且只有一個(gè)前驅(qū)節(jié)點(diǎn)和一個(gè)后續(xù)節(jié)點(diǎn)。
- 數(shù)據(jù)只能從棧的頂端插入和刪除
- 把數(shù)據(jù)放入棧頂稱為入棧(push)
- 從棧頂刪除數(shù)據(jù)稱為出棧(pop)
堆
堆是一塊內(nèi)存區(qū)域,與棧不同,堆里的內(nèi)存可以以任意順序存入和移除。
GC
- 垃圾回收的基本知識(shí)
GC
(Garbage Collector)垃圾回收器,是一種自動(dòng)內(nèi)存管理技術(shù),用于自動(dòng)釋放內(nèi)存。在.NET Framework
中,GC
由.NET
的運(yùn)行時(shí)環(huán)境CLR
自動(dòng)執(zhí)行。在公共語言運(yùn)行時(shí) (CLR) 中,垃圾回收器 (GC) 用作自動(dòng)內(nèi)存管理器。 垃圾回收器管理應(yīng)用程序的內(nèi)存分配和釋放。 因此,使用托管代碼的開發(fā)人員無需編寫執(zhí)行內(nèi)存管理任務(wù)的代碼。 自動(dòng)內(nèi)存管理可解決常見問題,例如,忘記釋放對(duì)象并導(dǎo)致內(nèi)存泄漏,或嘗試訪問已釋放對(duì)象的已釋放內(nèi)存。
通過GC
進(jìn)行自動(dòng)內(nèi)存管理得益于C#
是一種托管語言。C#
會(huì)將代碼編譯為托管代碼。托管代碼以中間語言(Intermediate Language, IL)的形式表示。CLR
通常會(huì)在執(zhí)行前,將IL
轉(zhuǎn)換為機(jī)器(例如x86或x64)原生代碼,稱為即時(shí)(Just-In-Time, JIT)編譯。除此之外,還可以使用提前編譯(ahead-of-time compilation)技術(shù)來改善擁有大程序集,或在資源有限的設(shè)備上運(yùn)行的程序的啟動(dòng)速度。
托管語言是一種在托管執(zhí)行環(huán)境中運(yùn)行的編程語言,該環(huán)境提供了自動(dòng)內(nèi)存管理、垃圾回收、類型檢查等服務(wù)。
托管執(zhí)行環(huán)境是指由操作系統(tǒng)提供的一種高級(jí)運(yùn)行時(shí)環(huán)境,例如Java虛擬機(jī)、.NET Framework、.NET Core 等。這種執(zhí)行環(huán)境為程序提供了許多優(yōu)勢,例如:
- 自動(dòng)內(nèi)存管理:托管執(zhí)行環(huán)境為程序管理內(nèi)存分配和釋放,程序員無需手動(dòng)管理內(nèi)存,避免了內(nèi)存泄漏和越界等問題。
- 垃圾回收:托管執(zhí)行環(huán)境提供了垃圾回收服務(wù),自動(dòng)回收不再使用的內(nèi)存,提高了程序的性能和可靠性。
- 類型檢查:托管執(zhí)行環(huán)境提供了強(qiáng)類型檢查,防止了類型錯(cuò)誤等問題。
- 平臺(tái)無關(guān)性:托管語言編寫的程序可以在不同操作系統(tǒng)和硬件平臺(tái)上運(yùn)行,提高了程序的可移植性。
在CLR
中:
- 每個(gè)進(jìn)程都有其自己單獨(dú)的虛擬地址空間。 同一臺(tái)計(jì)算機(jī)上的所有進(jìn)程共享相同的物理內(nèi)存和頁文件(如果有)。
- 默認(rèn)情況下,32 位計(jì)算機(jī)上的每個(gè)進(jìn)程都具有 2 GB 的用戶模式虛擬地址空間。
- 作為一名應(yīng)用程序開發(fā)人員,你只能使用虛擬地址空間,請(qǐng)勿直接操控物理內(nèi)存。 垃圾回收器為你分配和釋放托管堆上的虛擬內(nèi)存。
- 初始化新進(jìn)程時(shí),運(yùn)行時(shí)會(huì)為進(jìn)程保留一個(gè)連續(xù)的地址空間區(qū)域。 這個(gè)保留的地址空間被稱為托管堆。 托管堆維護(hù)著一個(gè)指針,用它指向?qū)⒃诙阎蟹峙涞南乱粋€(gè)對(duì)象的地址。
既然垃圾回收是自動(dòng)進(jìn)行的,那么一般什么時(shí)候GC
會(huì)開始回收垃圾呢?
- 系統(tǒng)具有低的物理內(nèi)存。內(nèi)存大小是通過操作系統(tǒng)的內(nèi)存不足通知或主機(jī)指示的內(nèi)存不足檢測出來的。
- 由托管堆上已分配的對(duì)象使用的內(nèi)存超出了可接受的閾值。 隨著進(jìn)程的運(yùn)行,此閾值會(huì)不斷地進(jìn)行調(diào)整。
- 調(diào)用 GC.Collect 方法。幾乎在所有情況下,你都不必調(diào)用此方法,因?yàn)槔厥掌鲿?huì)持續(xù)運(yùn)行。 此方法主要用于特殊情況和測試。
我們開發(fā)人員可以使用new
關(guān)鍵字在托管堆上動(dòng)態(tài)分配內(nèi)存,不需要手動(dòng)釋放,GC
會(huì)定期檢查托管堆上的對(duì)象,并回收掉沒有被引用的對(duì)象,從而釋放它們所占用的內(nèi)存。
???需要注意的是,棧內(nèi)存無需我們管理,同時(shí)它也不受
GC
管理。當(dāng)棧頂元素使用完畢以后,所占用的內(nèi)存會(huì)被立刻釋放。而堆則需要依賴于GC
清理。
值類型、引用類型
文章之前部分已經(jīng)提到過C#
是托管語言,在托管執(zhí)行環(huán)境中運(yùn)行的編程語言,該環(huán)境提供了強(qiáng)類型檢查,所以與其他語言相比,C#
對(duì)其可用的類型及其定義有更嚴(yán)格的描述 ———— C#
是一種強(qiáng)類型語言,每個(gè)變量和常量都有一個(gè)類型,每個(gè)求值的表達(dá)式也是如此。 每個(gè)方法聲明都為每個(gè)輸入?yún)?shù)和返回值指定名稱、類型和種類(值、引用或輸出)。
所有的C#
類型可以分為以下幾類:
-
值類型
-
引用類型
-
泛型類型
C#泛型可以是值類型也可以是引用類型,具體取決于泛型參數(shù)的類型。
如果泛型參數(shù)是值類型,那么實(shí)例化出來的泛型類型也是值類型。例如,
List<int>
就是一個(gè)值類型,因?yàn)?code>int是值類型。如果泛型參數(shù)是引用類型,那么實(shí)例化出來的泛型類型也是引用類型。例如,
List<string>
就是一個(gè)引用類型,因?yàn)?code>string是引用類型。需要注意的是,雖然泛型類型可以是值類型或引用類型,但是泛型類型的實(shí)例總是引用類型。這是因?yàn)樵趦?nèi)存中,泛型類型的實(shí)例始終是在堆上分配的,無論它的泛型參數(shù)是值類型還是引用類型。因此,使用泛型類型時(shí)需要注意它的實(shí)例是引用類型。
-
指針類型
指針類型是C#中的一種高級(jí)語言特性,允許程序員直接操作內(nèi)存地址。指針類型主要用于與非托管代碼交互、實(shí)現(xiàn)底層數(shù)據(jù)結(jié)構(gòu)等。指針類型在普通的C#代碼中并不常見。
撇去指針類型,我們可以把C#
中的數(shù)據(jù)類型分為兩種:
- 值類型 - 分兩類:
struct
和enum
,包括內(nèi)置的數(shù)值類型(所有的數(shù)值類型、char
類型和bool
類型)以及自定義的struct
類型和enum
類型。 - 引用類型 - 引用類型包含所有的類類型、接口類型、數(shù)組類型或委托類型。和值類型一樣,
C#
支持兩種預(yù)定義的引用類型:object
和string
。
???
object
類型是所有類型的基類型,其他類型都是從它派生而來的(包括值類型)。
各自在內(nèi)存中的存儲(chǔ)方式
在此之前,我們需要明白Windows
使用的是一個(gè)虛擬尋址系統(tǒng),該系統(tǒng)把程序可用的內(nèi)存地址映射到硬件內(nèi)存中的實(shí)際地址上,這些任務(wù)完全由Windows
在后臺(tái)管理。其實(shí)際結(jié)果是32位處理器上的每個(gè)進(jìn)程都可以使用4GB
的內(nèi)存————不管計(jì)算機(jī)上實(shí)際有多少物理內(nèi)存。這4個(gè)GB的內(nèi)存實(shí)際上包含了程序的所有部分,包括可執(zhí)行的代碼、代碼加載的所有DLL
,以及程序運(yùn)行時(shí)使用的所有變量的內(nèi)容。這4個(gè)GB的內(nèi)存稱為虛擬地址空間、虛擬內(nèi)存,我們這里簡稱它為內(nèi)存。
我們可以借助VS在直觀地體會(huì)這一特性,任意給個(gè)斷點(diǎn),把變量移到內(nèi)存窗口就可以查看當(dāng)前變量在內(nèi)存中的地址以及存儲(chǔ)的內(nèi)容:
例舉一些常用的變量:
// 值類型
int a = 123;
float b = 34.5f;
bool c = true;
// 引用類型
string name = "SiKi";
int[] array1 = new int[] { 23, 23, 11, 32, 4, 2435 };
string[] array2 = new string[] { "熊大", "熊二", "翠花" };
Customer customer = new Customer("光頭強(qiáng)", "狗熊嶺", 30, "2305507");
它們?cè)趦?nèi)存中是怎么存儲(chǔ)的呢?
- 值類型就直觀的存儲(chǔ)在堆中。
-
array1
在棧中存儲(chǔ)著一個(gè)指向堆中存放array1
數(shù)組首地址的引用,array2
和customer
同理 -
name
字符串,盡管它看上去像是一個(gè)值類型的賦值,但是它是一個(gè)引用類型,name
對(duì)象被分配在堆上。
關(guān)于字符串在內(nèi)存中的存儲(chǔ),雖然它是引用類型,但是它與引用類型的常見行為是有一些區(qū)別的,例如:字符串是不可變的。修改其中一個(gè)字符串,就會(huì)創(chuàng)建一個(gè)全新的string
對(duì)象,而對(duì)已存在的字符串不會(huì)產(chǎn)生任何影響。例如:
static void Main(string[] args)
{
string s1 = "a string";
string s2 = s1;
s1 = "another string";
Console.ReadKey();
}
借助VS的內(nèi)存窗口:
s1
也就是存儲(chǔ)著a string
字符串的地址是0x038023DC
,再執(zhí)行你就會(huì)發(fā)現(xiàn)s2
的內(nèi)存地址也是0x038023DC
,但是當(dāng)s1
中存儲(chǔ)的字符串發(fā)生變化時(shí),s1
的內(nèi)存地址也會(huì)隨之變化,但是s2
的內(nèi)存地址還是之前a string
所在的位置。
也就是說,字符串的值在發(fā)生變化時(shí)并不會(huì)替換原來的值,而是在堆上為新的字符串值分配一個(gè)新的對(duì)象(內(nèi)存空間),之前的字符串值對(duì)象是不受影響的【這實(shí)際上是運(yùn)算符重載的結(jié)果】。
To sum up,值類型直接存儲(chǔ)其值,而引用類型存儲(chǔ)對(duì)值的引用。這兩種類型存儲(chǔ)在內(nèi)存的不同地方:值類型存儲(chǔ)在棧(stack)中,而引用類型存儲(chǔ)在托管堆(managed heap)上。
- 值類型只需要一段內(nèi)存,總是分配在它聲明的地方,做為局部變量時(shí),存儲(chǔ)在棧上;假如是類對(duì)象的字段時(shí),則跟隨此類存儲(chǔ)在堆中。
- 引用類型需要兩段內(nèi)存,第一段存儲(chǔ)實(shí)際的數(shù)據(jù)【堆】,第二段是一個(gè)引用【?!?,用于指向數(shù)據(jù)在堆中的存儲(chǔ)位置。引用類型實(shí)例化的時(shí)候,會(huì)在托管堆上分配內(nèi)存給類的實(shí)例,類對(duì)象變量只保留對(duì)對(duì)象位置的引用,引用存放在棧中。
對(duì)象引用的改變
因?yàn)橐妙愋驮诖鎯?chǔ)的時(shí)候是兩段內(nèi)存,所以對(duì)于引用類型的對(duì)象的改變和值類型是不同的,以Customer
類的兩個(gè)對(duì)象為例:
static void Main(string[] args)
{
Customer c1 = new Customer("光頭強(qiáng)", "狗熊嶺", 30, "2305507");
Customer c2 = c1;
c1.Show();
c2.Show();
Console.WriteLine();
c2.address = "團(tuán)結(jié)屯";
c1.Show();
c2.Show();
Console.ReadKey();
}
執(zhí)行結(jié)果為:
名字:光頭強(qiáng)
地址:狗熊嶺
年齡:30
創(chuàng)建時(shí)間:2305507
名字:光頭強(qiáng)
地址:狗熊嶺
年齡:30
創(chuàng)建時(shí)間:2305507
名字:光頭強(qiáng)
地址:團(tuán)結(jié)屯
年齡:30
創(chuàng)建時(shí)間:2305507
名字:光頭強(qiáng)
地址:團(tuán)結(jié)屯
年齡:30
創(chuàng)建時(shí)間:2305507
可以發(fā)現(xiàn)當(dāng)我們修改了對(duì)象s2
中的address
字段以后s1
也跟著發(fā)生了變化,之所以這樣和引用類型在內(nèi)存中的存儲(chǔ)方式是密不可分的:
在創(chuàng)建s2
時(shí)并沒有和創(chuàng)建s1
一樣通過new
來創(chuàng)建一個(gè)全新的對(duì)象,而是通過=
賦值來的,因?yàn)橐妙愋痛鎯?chǔ)是二段存儲(chǔ),所以賦值以后s2
在棧中存儲(chǔ)的其實(shí)是s1
對(duì)象在堆中的存儲(chǔ)空間的地址,所以修改s2
的時(shí)候s1
也會(huì)隨之變化,因?yàn)槎咧赶虻氖峭粔K內(nèi)存空間。如果你通過new
關(guān)鍵字來實(shí)例化s2
,那s2
就是存儲(chǔ)的一個(gè)全新的Customer
對(duì)象了。感興趣可以看看不同方式創(chuàng)建的s2
對(duì)象在內(nèi)存中的地址一不一樣。
static void Main(string[] args)
{
Customer c1 = new Customer("光頭強(qiáng)", "狗熊嶺", 30, "2305507");
Customer c2 = new Customer("大熊", "東京", 14, "2309856");
Console.ReadKey();
}
這里面的s1
和s2
就存儲(chǔ)在兩段不同的內(nèi)存中。
繼承
本篇文章的標(biāo)題是“C# 面向?qū)ο蟆?,但是?code>C#并不是一種純粹的面向?qū)ο缶幊陶Z言,C#
中還包含一些非面向?qū)ο蟮奶匦?,比如靜態(tài)成員、靜態(tài)方法和值類型等,還支持一些其他的編程范式,比如泛型編程、異步編程和函數(shù)式編程。雖然但是,面向?qū)ο笕匀皇?code>C#中的一個(gè)重要概念,也是.NET
提供的所有庫的核心原則。
面向?qū)ο缶幊逃兴捻?xiàng)基本原則:
- 抽象:將實(shí)體的相關(guān)特性和交互建模為類,以定義系統(tǒng)的抽象表示。
- 封裝:隱藏對(duì)象的內(nèi)部狀態(tài)和功能,并僅允許通過一組公共函數(shù)進(jìn)行訪問。
- 繼承:根據(jù)現(xiàn)有抽象創(chuàng)建新抽象的能力。
- 多形性:跨多個(gè)抽象以不同方式實(shí)現(xiàn)繼承屬性或方法的能力?!径鄳B(tài)性】
在我們學(xué)習(xí)和使用類的過程中都或多或少在應(yīng)用抽象、封裝這些概念,或者說這些思想,我們之前都是在使用單個(gè)的某一個(gè)類,但在開發(fā)過程中,我們往往會(huì)遇到這樣一種情況:很多我們聲明的類中都有相似的數(shù)據(jù),比如一個(gè)游戲,里面有Boss
類、Enermy
類,這些類有很多相同的屬性,但是也有不同的,比方說Boss
和Enermy
都會(huì)飛龍?jiān)谔?,但?code>Boss還會(huì)烏鴉坐飛機(jī)這種高階技能等等,這個(gè)時(shí)候我們可以如果按照我們之前的思路,分別編寫了兩個(gè)類,假如飛龍?jiān)谔斓募寄鼙弧奥斆鞯摹辈邉潖U棄了或者調(diào)整了參數(shù),我們?cè)诰S護(hù)起來是很不方便的,這個(gè)時(shí)候就可以使用繼承來解決這個(gè)問題,它有父類和子類,相同的部分放在父類里就可以了。
繼承的類型:
-
由類實(shí)現(xiàn)繼承:
表示一個(gè)類型派生于一個(gè)基類型,它擁有該基類型的所有成員字段和函數(shù)。在實(shí)現(xiàn)繼承中,派生類型采用基類型的每個(gè)函數(shù)的實(shí)現(xiàn)代碼,除非在派生類型的定義中指定重寫某個(gè)函數(shù)的實(shí)現(xiàn)代碼。在需要給現(xiàn)有的類型添加功能,或許多相關(guān)的類型共享一組重要的公共功能時(shí),這種類型的繼承非常有用。
-
由接口實(shí)現(xiàn)繼承:
表示一個(gè)類型只繼承了函數(shù)的簽名,沒有繼承任何實(shí)現(xiàn)代碼。在需要指定該類型具有某些可用的特性時(shí),最好使用這種類型的繼承。
細(xì)說的話,繼承有單重繼承和多重繼承,單重繼承就是一個(gè)類派生自一個(gè)基類(C#
就是采用這種繼承),多重繼承就是一個(gè)類派生自多個(gè)類。
派生類也稱為子類(subclass);父類、基類也稱為超類(superclass)。
一些語言(例如C++
)是支持所謂的“多重繼承”的,但是關(guān)于多重繼承是有爭議的:一方面,多重繼承可以編寫更為復(fù)雜且較為緊湊的代碼;另一方面,使用多重繼承編寫的代碼一般很難理解和調(diào)試,也會(huì)產(chǎn)生一定的開銷。C#
的重要設(shè)計(jì)目標(biāo)就是簡化健壯代碼,所以C#
的設(shè)計(jì)人員決定不支持多重繼承。一般情況下,不使用多重繼承也是可以解決我們的問題的,所以很多編程語言,尤其是高級(jí)編程語言就不支持多重繼承了。
雖然C#
不支持多重繼承,但是C#
是允許一個(gè)類派生自多個(gè)接口的,這個(gè)后面章節(jié)再展開論述。
只需要知道,C#
中的類可以通過繼承另一個(gè)類來對(duì)自身進(jìn)行拓展或定制,子類可以繼承父類的所有函數(shù)成員和字段(繼承父類的所有功能而無需重新構(gòu)建),一個(gè)類只能有一個(gè)基類(父類),而且它只能繼承自唯一一個(gè)父類?但是,一個(gè)類可以被多個(gè)類繼承,這會(huì)使得類之間產(chǎn)生一定的層次,也被稱為多層繼承(C#
支持,并且很常用)。到這,你可能會(huì)想到,我們之前寫的聲明Customer
類啊或者Vehicle
啊它們有父類嘛?答案當(dāng)然是有的。就像在值類型、引用類型所說的,所有類型都有一個(gè)基類型就是Object
類,當(dāng)然了Object
可沒有基類,不能套娃嘛不是??
實(shí)現(xiàn)繼承
接著上面的游戲案例:
- 基類:敵人類 -
hp
speed
AI()
Move()
- 派生類:
boss
、type1enemy
、type2enemy
基類(父類):
class Enemy
{
protected int hp;
protected int speed;
public void AI() { Console.WriteLine("AI"); }
public void Move() { Console.WriteLine("Move"); }
}
protected
:僅允許在包含類或者子類中訪問
派生類Boss
(子類):
class Boss : Enemy
{
private int attack; // Boss的攻擊力比普通小兵攻擊力高
public Boss(int attack)
{
this.attack = attack;
}
public void Skill() { Console.WriteLine("Boss Skill"); }
public void Print()
{
Console.WriteLine("HP:"+hp);
Console.WriteLine("Speed:" + speed);
Console.WriteLine("Attack:" + attack);
}
}
創(chuàng)建一個(gè)Boss
對(duì)象,看一下:
static void Main(string[] args)
{
Boss boss = new Boss(100);
boss.Print();
//Output:
//HP:0
//Speed:0
//Attack:100
Console.ReadKey();
}
雖然可以訪問基類的字段,但是在創(chuàng)建對(duì)象的時(shí)候是沒有賦值的,使用的是默認(rèn)值,那怎樣才能在創(chuàng)建對(duì)象的時(shí)候也給基類的字段賦值呢?
this
關(guān)鍵字指代類的當(dāng)前實(shí)例嘛:
class Boss : Enemy
{
private int attack;
public Boss(int attack,int hp,int speed)
{
this.attack = attack;
this.hp = hp;
this.speed = speed;
}
public void Skill() { Console.WriteLine("Boss Skill"); }
public void Print()
{
Console.WriteLine("HP:"+hp);
Console.WriteLine("Speed:" + speed);
Console.WriteLine("Attack:" + attack);
}
}
用this
當(dāng)然是而可行的,但是除了this
之外,還有一個(gè)專門的關(guān)鍵字來幫助我們從派生類中訪問基類成員 - base
。
this 和 base 關(guān)鍵字
this
:代指當(dāng)前實(shí)例本身,可避免字段、局部變量或?qū)傩灾g發(fā)生混淆。this
引用僅在類或結(jié)構(gòu)體的非靜態(tài)成員中有效。
base
:用于從派生類中訪問基類成員,它有兩個(gè)重要作用:
? - 調(diào)用基類上已被其他方法重寫的方法。
? - 指定創(chuàng)建派生類實(shí)例時(shí)應(yīng)調(diào)用的基類構(gòu)造函數(shù)。
使用base
關(guān)鍵字的構(gòu)造函數(shù):
private int attack;
public Boss(int attack,int hp,int speed)
{
this.attack = attack;
base.hp = hp;
base.speed = speed;
}
有什么好處呢,加入派生類中的字段和基類中的字段一樣時(shí),就可以通過這種方式來避免混淆。假設(shè)我們的派生類Boss
里面也有個(gè)hp
的字段:
class Boss : Enemy
{
private int attack;
private int hp;
public Boss(int attack,int hp,int speed)
{
this.attack = attack;
//this.hp = hp;
//this.speed = speed;
base.hp = hp;
base.speed = speed;
}
public void Skill() { Console.WriteLine("Boss Skill"); }
public void Print()
{
Console.WriteLine("HP:"+hp);
Console.WriteLine("Base.HP:"+base.hp);
Console.WriteLine("Speed:" + speed);
Console.WriteLine("Attack:" + attack);
}
}
我們?cè)賱?chuàng)建一個(gè)實(shí)例:
傳進(jìn)去的hp
的值是根據(jù) base.hp = hp;
給到了基類中的hp
字段。但是一般情況下,不推薦派生類中和基類重名的字段,一般在子類重寫父類方法時(shí)通過base
關(guān)鍵字區(qū)分,就是上面說的第一種應(yīng)用。
通過base
關(guān)鍵字是沒辦法訪問attck
字段的,base.attack
會(huì)報(bào)錯(cuò)。
base
所訪問的基類是類聲明中指定的基類,不能是多級(jí)訪問。
重載和重寫
重載(Overloading):指的是在同一個(gè)類中定義多個(gè)具有相同名稱但參數(shù)列表不同的方法。通過重載,可以在同一個(gè)類中創(chuàng)建多個(gè)方法,它們執(zhí)行相似的操作但接受不同類型或數(shù)量的參數(shù)。編譯器會(huì)根據(jù)調(diào)用時(shí)提供的參數(shù)類型和數(shù)量來確定調(diào)用哪個(gè)重載方法。重載可以提高代碼的可讀性和靈活性,允許在不同的情況下使用相同的方法名進(jìn)行不同的操作。
例如,在一個(gè)名為Calculator
的類中可以定義多個(gè)名為add
的方法,如下所示:
class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
上述代碼中,Calculator
類定義了兩個(gè)重載的add
方法,一個(gè)用于整數(shù)相加,另一個(gè)用于浮點(diǎn)數(shù)相加。根據(jù)提供的參數(shù)類型,編譯器會(huì)選擇適合的重載方法。
??????注意
方法簽名由方法名、參數(shù)數(shù)量和參數(shù)類型共同決定,方法的返回類型不計(jì)入簽名。兩個(gè)同名方法如果獲取相同的參數(shù)列表,就說它們有相同的簽名,即使它們的返回類型不同。
重寫(Overriding):指的是子類重新實(shí)現(xiàn)(覆蓋)了從父類繼承的方法,以改變方法的行為。當(dāng)子類需要修改父類中的方法實(shí)現(xiàn)時(shí),可以使用方法重寫。在重寫過程中,子類需要使用相同的方法名稱、相同的參數(shù)列表和相同或更寬松的訪問修飾符來重新定義父類的方法。重寫方法可以提供特定于子類的實(shí)現(xiàn)。
以下是一個(gè)簡單的例子,展示了父類Animal
和子類Cat
之間的方法重寫:
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal makes sound");
}
}
class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Cat meows");
}
}
在上述代碼中,Animal
類定義了一個(gè)名為MakeSound
的方法,而Cat
類繼承自Animal
類并重寫了MakeSound
方法。當(dāng)調(diào)用MakeSound
方法時(shí),如果對(duì)象是Cat
類的實(shí)例,將執(zhí)行Cat
類中的重寫方法,輸出"Cat meows";否則,將執(zhí)行父類Animal
的原始方法,輸出"Animal makes sound"。
總結(jié)來說,重載用于在同一個(gè)類中定義多個(gè)具有相同名稱但參數(shù)列表不同的方法,以便在不同情況下執(zhí)行不同的操作。而重寫用于子類重新實(shí)現(xiàn)(覆蓋)繼承自父類的方法,以改變方法的行為。
我感覺這倆兄弟八竿子打不著,但是我一開始理解錯(cuò)了,所以Mark
一下??????
隱藏成員
編程最困難的地方之一是為標(biāo)識(shí)符想一個(gè)獨(dú)特的、有意義的名稱,如果基類和派生類同時(shí)聲明了兩個(gè)具有相同簽名的方法,編譯時(shí)會(huì)顯示一個(gè)警告。比如下面的例子中,當(dāng)我們?cè)?code>Boss類中重新定義一個(gè)Move
方法,編譯器會(huì)顯示警告消息指出Boss.Move()
隱藏了繼承的成員Enemy.Move()
,也就是說Boss
繼承的基類Enemy
的Move
方法就會(huì)被隱藏掉:
這個(gè)時(shí)候創(chuàng)建一個(gè)Boss
對(duì)象并調(diào)用Move
方法會(huì)是在Boss
類中重新定義的方法:
雖然代碼能編譯并運(yùn)行,但是應(yīng)該嚴(yán)肅對(duì)待該警告。如果有另一個(gè)派生自Boss
類并調(diào)用了Move
方法,它想要調(diào)用的可能是Enemy
類實(shí)現(xiàn)的Move
方法,但是這被Boss
類中的Move
隱藏了,所以實(shí)際調(diào)用的是后者。大多數(shù)時(shí)候,像這樣的巧合會(huì)成為混亂之源。應(yīng)重命名方法以免沖突。但如果確實(shí)希望兩個(gè)方法具有相同簽名,從而隱藏Enemy
的Move
方法,可明確使用new
關(guān)鍵字消除警告,告訴編譯器我知道自己在干什么:
class Boss : Enemy
{
public int attack; // Boss的攻擊力比普通小兵攻擊力高
public Boss(int attack, int hp, int speed)
{
this.attack = attack;
base.hp = hp;
base.speed = speed;
}
public new void Move()
{
Console.WriteLine("Boss Move");
}
...
虛方法
什么是虛方法?
虛方法(Virtual method)是一種允許子類重寫(覆蓋)父類以實(shí)現(xiàn)方法的方法。通過聲明方法為虛方法,可以在父類中定義一個(gè)方法,并允許子類通過重寫該方法來提供自己的實(shí)現(xiàn)。在父類中,可以使用 virtual
關(guān)鍵字來聲明一個(gè)方法為虛方法。虛方法允許子類通過使用 override
關(guān)鍵字來重寫該方法,以便子類可以根據(jù)自身的需要改變方法的行為。
為什么需要虛方法?
為了隱藏方法在基類中的實(shí)現(xiàn)??這么說可能比較不好理解,我們舉個(gè)例子,在開始學(xué)習(xí)C#
的時(shí)候大家可能會(huì)注意到C#
中萬物都可ToString()
,任何類型的實(shí)例都可以通過調(diào)用ToString
方法將自身轉(zhuǎn)換為一個(gè)字符串,這得益于C#
擁有統(tǒng)一的類型系統(tǒng),其所有類型都共享一個(gè)公共的基類Object
,因?yàn)樗苡杏盟栽O(shè)計(jì)者把它作為Object
的成員自動(dòng)提供給所有類。我們看看boss
實(shí)例在字符串之后會(huì)輸出什么:
??輸出內(nèi)容并不盡如人意,那ToString
是如何將實(shí)例轉(zhuǎn)換成字符串的呢?派生類(指的是所有類都派生于基類System.Object
)中可能包含任意數(shù)量的字段,這些字段包含的值應(yīng)該是字符串的一部分,但是System.Object
中實(shí)現(xiàn)的ToString
太過于簡單,它唯一能做的就是將對(duì)象轉(zhuǎn)化成其類型名稱的字符串,就像圖片中輸出的那樣CSharpTutorial_01.Program+Boss
,這種轉(zhuǎn)換是毫無意義的,那為什么要提供一個(gè)沒有用的方法呢?
顯然,ToString
是一個(gè)很好的概念,所有類都應(yīng)當(dāng)提供一個(gè)方法將對(duì)象轉(zhuǎn)換成字符串,以便于查看和調(diào)試。事實(shí)上,System.Object
中實(shí)現(xiàn)的ToString
只是一個(gè)“占位符”,我們應(yīng)該在每一個(gè)自定義類中提供自己的ToString
方法 ———— 重寫基類System.Object
中的ToString
方法。
我們選中ToString
進(jìn)入到定義可以發(fā)現(xiàn),在基類System.Object
中ToString
故意設(shè)計(jì)成要被重寫的帶有virtual
的虛方法。

接下來我們?cè)?code>Boss中實(shí)現(xiàn)我們自己的ToString
方法(當(dāng)你在VS中拼完override`關(guān)鍵字后智能提示會(huì)顯示當(dāng)前可重寫的方法列表,并且選中后會(huì)有一個(gè)默認(rèn)實(shí)現(xiàn)):
我們通過該方法顯示當(dāng)前對(duì)象的hp
、attack
、speed
:
class Boss : Enemy
{
private int attack;
private int hp;
public Boss(int attack, int hp, int speed)
{
this.attack = attack;
base.hp = hp;
base.speed = speed;
}
// . . .
public override string ToString()
{
return "HP: " + base.hp + " " + "Attack: " + this.attack + " " + "Speed: " + base.speed;
}
}
故意設(shè)計(jì)成要被重寫的方法稱為虛(virtual)方法?!爸貙懛椒ā焙汀半[藏方法(隱藏成員)”的區(qū)別現(xiàn)在應(yīng)該很明顯了。重寫是提供同一個(gè)方法的不同實(shí)現(xiàn),這些方法有關(guān)系,因?yàn)槎贾荚谕瓿上嗤娜蝿?wù),只是不同的類用不同的方式。但隱藏是指方法被替換成另一個(gè)方法,方法通常沒關(guān)系,而且可能執(zhí)行完全不同的任務(wù)。對(duì)方法進(jìn)行重寫是有用的編程概念:而如果方法被隱藏,則意味著可能發(fā)生了一處編程錯(cuò)誤(除非你加上new
強(qiáng)調(diào)自己沒錯(cuò))。
Object.ToString 方法
虛方法和多態(tài)性
多態(tài)性(Polymorphism)是面向?qū)ο缶幊讨械囊粋€(gè)重要概念,指的是在運(yùn)行時(shí)能夠根據(jù)對(duì)象的實(shí)際類型來執(zhí)行不同的操作。
在多態(tài)性中,重要的概念是虛方法和方法重寫。通過在父類中聲明虛方法,并在子類中使用 override
關(guān)鍵字重寫該方法,可以實(shí)現(xiàn)多態(tài)性。當(dāng)使用基類類型的引用變量引用派生類對(duì)象并調(diào)用虛方法時(shí),實(shí)際上會(huì)根據(jù)對(duì)象的實(shí)際類型來選擇執(zhí)行哪個(gè)重寫方法。
多態(tài)性允許使用基類類型的引用變量來引用派生類對(duì)象,并根據(jù)對(duì)象的實(shí)際類型來調(diào)用相應(yīng)的方法。這使得我們可以在不同的對(duì)象上執(zhí)行相同的操作,而不需要針對(duì)每個(gè)具體的對(duì)象類型編寫單獨(dú)的代碼。這樣做的好處是,在編寫代碼時(shí),我們不需要針對(duì)每個(gè)具體的對(duì)象類型編寫單獨(dú)的代碼。我們可以使用一個(gè)通用的代碼塊來處理所有派生類對(duì)象,只需要使用基類類型的引用變量來引用它們,并調(diào)用相同的方法。
接著上面的游戲案例,除了boss
不是還有type1enemy
、type2enemy
兩個(gè)派生類嗎,假如說boss
、type1enemy
、type2enemy
都有各自不同于基類Enemy
的行動(dòng)方式,也就是Move
方法,這時(shí)候就可以用到多態(tài)性():
class Enemy
{
protected int hp;
protected int speed;
public void AI() { Console.WriteLine("AI"); }
public virtual void Move() { Console.WriteLine("Move"); }
}
class Boss : Enemy
{
public int attack; // Boss的攻擊力比普通小兵攻擊力高
public override void Move()
{
Console.WriteLine("Boss Move");
}
}
class Type1enemy : Enemy
{
public override void Move()
{
Console.WriteLine("type1enemy Move");
}
}
class Type2enemy : Enemy
{
public override void Move()
{
Console.WriteLine("type2enemy Move");
}
}
通過多態(tài)性,我們可以使用基類類型的引用變量來引用不同派生類的對(duì)象,然后調(diào)用它們的Move()
方法:
internal class Program
{
static void Main(string[] args)
{
Enemy boss = new Boss();
boss.Move();
Enemy enemy1 = new Type1enemy();
enemy1.Move();
enemy1 = new Type2enemy();
enemy1.Move();
Console.ReadKey();
}
}
執(zhí)行結(jié)果為:
Boss Move
type1enemy Move
type2enemy Move
這種方式使得我們的代碼更加靈活和可擴(kuò)展。當(dāng)我們新增一個(gè)派生類時(shí),只需要讓它繼承自基類并重寫基類中的方法,然后我們就可以在通用的代碼中使用基類類型的引用變量來引用新的派生類對(duì)象,并調(diào)用相同的方法,無需修改通用的代碼塊。
總結(jié)來說,多態(tài)性通過使用基類類型的引用變量來引用派生類對(duì)象,并根據(jù)對(duì)象的實(shí)際類型來選擇執(zhí)行相應(yīng)的方法,實(shí)現(xiàn)了在不同的對(duì)象上執(zhí)行相同操作的便利性,減少了代碼的重復(fù)和冗余。
抽象方法和抽象類
抽象類(Abstract class)是一種特殊的類,它不能被實(shí)例化,只能被用作其他類的基類。抽象類用于定義一組相關(guān)的類的公共結(jié)構(gòu)和行為,并可以包含抽象成員(抽象方法、抽象屬性等)和非抽象成員。
抽象類通過在類定義前面加上 abstract
關(guān)鍵字來聲明。抽象類可以包含普通方法的實(shí)現(xiàn)和抽象方法的定義。抽象方法是沒有具體實(shí)現(xiàn)的方法,只有方法的簽名(返回類型、方法名和參數(shù)列表),并且在派生類中必須進(jìn)行重寫。顯然,抽象方法本身也是虛擬Virtual
的(雖然不需要提供virtual
關(guān)鍵字,事實(shí)上,如果寫了該關(guān)鍵字,程序會(huì)產(chǎn)生一個(gè)語法錯(cuò)誤??)。如果類包含抽象方法,那么這個(gè)類就是抽象的,并且必須聲明為抽象類。
那么什么時(shí)候用到抽象類呢?繼續(xù)之前的游戲案例,敵人有很多種,都會(huì)攻擊,但是每個(gè)人的攻擊方式都不一樣,在父類Enemy
中聲明的Attack
來表示攻擊,這個(gè)時(shí)候我們?cè)诟割惱镪U述清楚關(guān)于Attack
的詳細(xì)定義是沒有用處的,因?yàn)槲覀兌贾烂總€(gè)敵人的攻擊方式是不一樣的,即使我們聲明定義好也需要在子類中根據(jù)子類的種類來定義不同的攻擊方式,這個(gè)時(shí)候,我們只需要在父類中有一個(gè)“攻擊方式”的占位符就行了,不必要具體實(shí)現(xiàn),就和上面說的ToString
方法類似(當(dāng)然也是有些細(xì)微區(qū)別的哈)。
如果一個(gè)方法在抽象類中提供默認(rèn)實(shí)現(xiàn)沒有意義,但有需要派生類提供該方法夫人實(shí)現(xiàn),那這個(gè)方法就適合定義成抽象方法:
public abstract void Attack();
這樣寫了之后我們會(huì)發(fā)現(xiàn)編譯器會(huì)報(bào)錯(cuò):
這就是上面說的,如果類包含抽象方法,那么這個(gè)類就是抽象類,并且必須聲明為抽象類:
abstract class Enemy
{
public abstract void Attack();
}
可以認(rèn)為,抽象方法是不完整的,因?yàn)樗某橄蟪蓡T是不完整的,需要在每個(gè)派生類中完成定義。當(dāng)創(chuàng)建抽象類的實(shí)例時(shí)是錯(cuò)誤的,因?yàn)轭愔械某蓡T是不完整的,當(dāng)然了聲明一個(gè)Enemy
對(duì)象是可以的,但是實(shí)例化不可以,可以通過它的派生類來完成構(gòu)造。
我們完善一下Enemy
類內(nèi)容:
abstract class Enemy
{
private int hp;
private int speed;
public void Move() { Console.WriteLine("Move"); }
public abstract void Attack();
}
接著來實(shí)現(xiàn)一個(gè)Boss
派生類,按照以往的繼承方法Boss
在繼承基類Enemy
后會(huì)報(bào)錯(cuò):
這個(gè)時(shí)候我們可以通過那個(gè)小燈泡??快速實(shí)現(xiàn)抽象類,然后實(shí)現(xiàn)各自的方法就可以了:
class Boss : Enemy
{
public override void Attack()
{
Console.WriteLine("Boss Attack");
}
}
以面向?qū)ο笏枷耄貜?fù)的代碼是警告的信號(hào),應(yīng)該重構(gòu)以免重復(fù)并減少維護(hù)開銷。
密封類和密封方法
實(shí)際開發(fā)中使用較少,但是語法簡單。/
如果不想一個(gè)類作為基類,可以使用sealed(密封)
關(guān)鍵字防止類被用作基類。如果你不想讓子類重寫某個(gè)方法,可以添加sealed(密封)
來防止子類重寫該方法。
- 防止代碼混亂
- 商業(yè)原因
派生類的構(gòu)造函數(shù)
前面章節(jié)中我們介紹過單個(gè)類的構(gòu)造函數(shù)如果定義以及如何工作的,還有在繼承的時(shí)候如果通過base
關(guān)鍵字訪問基類字段完成初始化,我們知道,繼承除了得到方法派生類還會(huì)自動(dòng)包含來自基類的所有字段,這寫字段通常需要初始化。此外,所有類都至少有一個(gè)構(gòu)造器(沒有顯示聲明的話,編譯器會(huì)自動(dòng)生成一個(gè)無參的默認(rèn)構(gòu)造器),當(dāng)我們?cè)谂缮惗x構(gòu)造函數(shù)的時(shí)候可以通過base
關(guān)鍵字調(diào)用基類的構(gòu)造函數(shù):
class BaseClass
{
public BaseClass()
{
Console.WriteLine("基類 構(gòu)造函數(shù)");
}
}
class DrivedClass : BaseClass
{
public DrivedClass() :base()
{
Console.WriteLine("派生類 構(gòu)造函數(shù)");
}
}
初始化一個(gè)DrivedClass
實(shí)例后先后調(diào)用,注意是先調(diào)用基類的構(gòu)造函數(shù):
如果我們不寫base()
也會(huì)調(diào)用基類的構(gòu)造函數(shù)的,因?yàn)槟憷^承了。。。
上面是無參的情況,那有參的情況呢?
class BaseClass
{
private string name;
private string description;
public BaseClass()
{
Console.WriteLine("基類 構(gòu)造函數(shù)");
}
public BaseClass(string name, string description)
{
this.name = name;
this.description = description;
}
}
class DrivedClass : BaseClass
{
private int age;
public DrivedClass(int age,string name,string description):base(name,description)
{
this.age = age;
}
public DrivedClass() :base()
{
Console.WriteLine("派生類 構(gòu)造函數(shù)");
}
}
修飾符
- 修飾符
前面遇到過很多修飾符,或修飾類、類成員,或指定方法得到可見性,又或是指定其本質(zhì) - virtual
、abstract
,C#
有許多修飾符。
訪問修飾符
- public:同一程序集中的任何其他代碼或引用該程序集的其他程序集都可以訪問該類型或成員。 某一類型的公共成員的可訪問性水平由該類型本身的可訪問性級(jí)別控制。
-
private:只有同一
class
或struct
中的代碼可以訪問該類型或成員。 -
protected:只有同一
class
或者從該class
派生的class
中的代碼可以訪問該類型或成員。 -
internal:同一程序集中的任何代碼都可以訪問該類型或成員,但其他程序集中的代碼不可以。 換句話說,
internal
類型或成員可以從屬于同一編譯的代碼中訪問。 -
protected internal:該類型或成員可由對(duì)其進(jìn)行聲明的程序集或另一程序集中的派生
class
中的任何代碼訪問。 -
private protected:該類型或成員可以通過從
class
派生的類型訪問,這些類型在其包含程序集中進(jìn)行聲明。
public
和private
修飾字段和方法的時(shí)候,表示該字段或者方法能不能通過對(duì)象去訪問,只有public
的才可以通過對(duì)象訪問,private(私有的)
只能在類內(nèi)部訪問。protected保護(hù)的
,當(dāng)沒有繼承的時(shí)候,它的作用和private
是一樣的,當(dāng)有繼承的時(shí)候,protected
:表示可以被子類訪問的字段或者方法
其他修飾符
-
abstract:使用
abstract
修飾的類為抽象類,抽象類只能是其他類的基類,不能與sealed
、static
一起使用。abstract
可以修飾抽象類中的方法或?qū)傩?,此時(shí),方法或?qū)傩圆荒馨瑢?shí)現(xiàn),且訪問級(jí)別不能為私有。抽象類不能被實(shí)例化。
-
sealed:使用
sealed
修飾的類為密封類,密封類無法被繼承,不能和abstract
、static
一起使用。當(dāng)
sealed
用于方法或?qū)傩詴r(shí),必須始終與override
一起使用。 -
static:使用
static
修飾的類為靜態(tài)類,靜態(tài)類所有成員都必須是靜態(tài)的,不能與abstract
、sealed
一起使用。static
可以修飾方法、字段、屬性或事件,始終通過類名而不是實(shí)例名稱訪問靜態(tài)成員,靜態(tài)字段只有一個(gè)副本。靜態(tài)類不能被實(shí)例化。
-
const:使用
const
關(guān)鍵字來聲明某個(gè)常量字段或常量局部變量,必須在聲明常量時(shí)賦初值。不能與
static
一起使用,常量默認(rèn)是static
的,常量字段只有一個(gè)副本。 -
readonly:使用
readonly
關(guān)鍵字來聲明只讀字段。只讀字段可以在聲明或構(gòu)造函數(shù)中初始化,每個(gè)類或結(jié)構(gòu)的實(shí)例都有一個(gè)獨(dú)立的副本。
可以與
static
一起使用,聲明靜態(tài)只讀字段。靜態(tài)只讀字段可以在聲明或靜態(tài)構(gòu)造函數(shù)中初始化,靜態(tài)常量字段只有一個(gè)副本。
-
virtual:
virtual
關(guān)鍵字用于修飾方法、屬性、索引器或事件聲明,并使它們可以在派生類中被重寫。默認(rèn)情況下,方法是非虛擬的。 不能重寫非虛方法。
virtual
修飾符不能與static
、abstract
、private
或override
修飾符一起使用。 -
override:要擴(kuò)展或修改繼承的方法、屬性、索引器或事件的抽象實(shí)現(xiàn)或虛實(shí)現(xiàn),必須使用
override
修飾符。重寫的成員必須是
virtual
、abstract
或override
的。
C#常用修飾符
關(guān)于static
在C#
中,static
關(guān)鍵字用于聲明靜態(tài)成員,這意味著它們與類相關(guān)而不是與類的實(shí)例(對(duì)象)相關(guān)。以下是static
關(guān)鍵字的一些常見用法:
-
靜態(tài)字段(Static Fields): 靜態(tài)字段是與類相關(guān)聯(lián)的字段,而不與類的實(shí)例相關(guān)聯(lián)。它們?cè)陬惖乃袑?shí)例之間共享相同的值。靜態(tài)字段可以通過類名直接訪問,而無需創(chuàng)建類的實(shí)例。下面是一個(gè)靜態(tài)字段的示例:
class Counter { public static int Count; // 靜態(tài)字段 public Counter() { Count++; } } Console.WriteLine(Counter.Count); // 輸出:0 Counter counter1 = new Counter(); Console.WriteLine(Counter.Count); // 輸出:1 Counter counter2 = new Counter(); Console.WriteLine(Counter.Count); // 輸出:2
-
靜態(tài)方法(Static Methods): 靜態(tài)方法是屬于類而不是類的實(shí)例的方法。它們可以直接通過類名調(diào)用,無需創(chuàng)建類的實(shí)例。靜態(tài)方法通常用于執(zhí)行與類相關(guān)的任務(wù),而不需要訪問實(shí)例的狀態(tài)。下面是一個(gè)靜態(tài)方法的示例:
class MathUtils { public static int Add(int a, int b) // 靜態(tài)方法 { return a + b; } } int result = MathUtils.Add(5, 3); // 調(diào)用靜態(tài)方法 Console.WriteLine(result); // 輸出:8
??在靜態(tài)方法中只能訪問和使用靜態(tài)成員
-
靜態(tài)類(Static Classes): 靜態(tài)類是一種特殊類型的類,它只包含靜態(tài)成員,并且不能被實(shí)例化。靜態(tài)類通常用于提供一組相關(guān)的靜態(tài)方法和工具函數(shù),不如
Math
類。下面是一個(gè)靜態(tài)類的示例:static class StringUtils { public static bool IsNullOrEmpty(string str) { return string.IsNullOrEmpty(str); } } bool isEmpty = StringUtils.IsNullOrEmpty(""); // 調(diào)用靜態(tài)方法 Console.WriteLine(isEmpty); // 輸出:True
請(qǐng)注意,靜態(tài)成員只能訪問其他靜態(tài)成員,不能直接訪問實(shí)例成員。而實(shí)例成員可以訪問靜態(tài)成員。
靜態(tài)字段在內(nèi)存中存儲(chǔ)在特定的位置,這取決于它們的訪問修飾符和作用域。
對(duì)于靜態(tài)字段,它們的存儲(chǔ)位置有兩種情況:
- 靜態(tài)字段存儲(chǔ)在靜態(tài)數(shù)據(jù)區(qū)(Static Data Area): 當(dāng)靜態(tài)字段是類的靜態(tài)成員時(shí),它們存儲(chǔ)在靜態(tài)數(shù)據(jù)區(qū)。靜態(tài)數(shù)據(jù)區(qū)在程序啟動(dòng)時(shí)分配,并在整個(gè)程序執(zhí)行期間保持不變。靜態(tài)字段的內(nèi)存分配在程序開始運(yùn)行時(shí)進(jìn)行,當(dāng)程序結(jié)束時(shí),靜態(tài)數(shù)據(jù)區(qū)的內(nèi)存會(huì)被釋放。
- 靜態(tài)字段存儲(chǔ)在元數(shù)據(jù)區(qū)(Metadata Area): 當(dāng)靜態(tài)字段是類的常量成員(使用
const
修飾符)時(shí),它們存儲(chǔ)在元數(shù)據(jù)區(qū)。元數(shù)據(jù)區(qū)是用于存儲(chǔ)類型信息和常量的地方,它在程序編譯時(shí)就被確定,并隨著程序的執(zhí)行一直存在。
無論靜態(tài)字段存儲(chǔ)在靜態(tài)數(shù)據(jù)區(qū)還是元數(shù)據(jù)區(qū),它們都具有全局可見性,可以在程序的任何地方訪問。
需要注意的是,靜態(tài)字段是與類相關(guān)聯(lián)的,而不是與類的實(shí)例相關(guān)聯(lián)。這意味著所有類的實(shí)例共享相同的靜態(tài)字段,它們?cè)趦?nèi)存中只有一份副本。
此外,靜態(tài)字段的生命周期和應(yīng)用程序的生命周期一致。它們?cè)趹?yīng)用程序啟動(dòng)時(shí)初始化,并在應(yīng)用程序關(guān)閉時(shí)銷毀。
接口
接口(Interface)是一種在C#
中定義協(xié)定(Contract)的方式,它描述了類或結(jié)構(gòu)體應(yīng)該具有的成員(方法、屬性、事件等)。接口定義了一組公共行為,但不提供實(shí)現(xiàn)細(xì)節(jié)。
接口在很多方面和抽象類類似,聲明接口在語法上和聲明抽象類完全相同,但是在接口中不允許提供任何成員的實(shí)現(xiàn)方式,它是純抽象的。此外,接口既不能有構(gòu)造函數(shù)也不能有字段,接口的定義也不允許有運(yùn)算符的重載,接口成員總是隱式Public
的,也不能聲明成員的修飾符,比如Virtual
。如果需要的話,應(yīng)該由實(shí)現(xiàn)的類來完成。
??接口和抽象類在構(gòu)造函數(shù)上是不同的,雖然二者都不能創(chuàng)建其對(duì)應(yīng)的實(shí)例,但是抽象類可以定義構(gòu)造函數(shù),只不過只能在創(chuàng)建子類實(shí)例時(shí)才可以調(diào)用。
在C#
中,接口使用 interface
關(guān)鍵字定義,可以包含方法、屬性、事件和索引器等成員的聲明。接口中的成員默認(rèn)是公共的,并且不能包含字段或?qū)崿F(xiàn)代碼。類可以實(shí)現(xiàn)一個(gè)或多個(gè)接口,表示類承諾實(shí)現(xiàn)接口中定義的所有成員。
接口名稱前面一般添加 "I" 來表示接口的特殊性,這是一種常見的命名約定。例如,IShape
表示一個(gè)形狀接口。
以下是一個(gè)簡單的接口示例,另外很多操作我們都可以借助VS的Intelligence
快速完成:

namespace CSharpTutorial_01
{
internal class Program
{
static void Main(string[] args)
{
Eagle eagle = new Eagle();
eagle.Fly();
eagle.FlyAttack();
IFly fly = new Eagle();
fly.Fly();
fly.FlyAttack();
fly = new Bird();
fly .Fly();
fly.FlyAttack();
Console.ReadKey();
}
}
interface IFly
{
void Fly(); // 飛翔
void FlyAttack(); // 虛空打擊
}
class Eagle : IFly
{
public void Fly()
{
Console.WriteLine("飛鷹展翅");
}
public void FlyAttack()
{
Console.WriteLine("龍卷風(fēng)摧毀停車場");
}
}
class Bird : IFly
{
public void Fly()
{
Console.WriteLine("怒鴉飛行");
}
public void FlyAttack()
{
Console.WriteLine("烏鴉坐飛機(jī)");
}
}
}
IFly
相當(dāng)簡單的接口,只定義了兩個(gè)方法。大多數(shù)接口都包含許多成員,執(zhí)行結(jié)果:

某個(gè)模塊需要包含若干個(gè)功能,這個(gè)時(shí)候就可以將這些功能放在一個(gè)接口中,如果某個(gè)類想要擁有這個(gè)功能的話就自行去實(shí)現(xiàn)這個(gè)接口就可以了。
接口的繼承
接口可以彼此繼承,其方式與類的繼承方式相同。哦,對(duì)了,記得C#
里派生類只能繼承自一個(gè)基類,但是可以繼承多個(gè)接口也就是多接口實(shí)現(xiàn),只要老老實(shí)實(shí)實(shí)現(xiàn)每個(gè)接口就行啦。接口繼承語法很簡單,繼承哪個(gè)接口就:Ixxxxx
就可以了,只要在實(shí)現(xiàn)這個(gè)接口的那個(gè)類里面需要把繼承的接口也實(shí)現(xiàn):
internal class Program
{
static void Main(string[] args)
{
Pterosaur pterosaur = new Pterosaur();
pterosaur.Run();
pterosaur.Fly();
Console.ReadKey();
}
}
interface IFly
{
void Fly(); // 飛翔
void FlyAttack(); // 虛空打擊
}
interface IRun:IFly
{
void Run();
}
class Pterosaur : IRun
{
public void Fly()
{
Console.WriteLine("Fly");
}
public void FlyAttack()
{
Console.WriteLine("FlyAttack");
}
public void Run()
{
Console.WriteLine("Run");
}
}
接口繼承不太常用。
索引器
不太常用
索引器(Indexer)允許通過類實(shí)例的類似數(shù)組的語法來訪問對(duì)象的元素。索引器允許在類內(nèi)部定義一個(gè)特殊的訪問器(Getter
和Setter
),通過索引參數(shù)來獲取或設(shè)置對(duì)象中的元素。
使用索引器,可以像使用數(shù)組一樣通過索引訪問對(duì)象中的元素,這使得對(duì)象可以按照一定的順序組織和訪問數(shù)據(jù)。索引器提供了一種方便的方式來訪問和操作對(duì)象的元素,增加了代碼的可讀性和易用性。
以下是一個(gè)使用索引器的簡單示例:
class MyList
{
private int[] data;
public MyList()
{
data = new int[10];
}
// 索引器的定義
public int this[int index]
{
get
{
return data[index];
}
set
{
data[index] = value;
}
}
}
// 使用索引器訪問對(duì)象的元素
MyList myList = new MyList();
myList[0] = 1; // 設(shè)置索引為0的元素的值
int value = myList[0]; // 獲取索引為0的元素的值
在上面的示例中,MyList
類定義了一個(gè)索引器,它允許通過整數(shù)索引訪問內(nèi)部的 data
數(shù)組。索引器的訪問器使用 get
和 set
關(guān)鍵字來定義獲取和設(shè)置元素的邏輯。
通過使用索引器,可以通過類似 myList[0]
的語法來訪問 MyList
對(duì)象中的元素,就像訪問數(shù)組元素一樣。在索引器的背后,實(shí)際上是調(diào)用了索引器的訪問器方法。
需要注意的是,索引器可以具有多個(gè)參數(shù),以便實(shí)現(xiàn)多維索引或具有其他復(fù)雜的訪問邏輯。此外,一個(gè)類可以定義多個(gè)索引器,只要它們的參數(shù)類型和個(gè)數(shù)不同即可。
運(yùn)算符重載
不太常用
運(yùn)算符重載(Operator Overloading)允許我們?yōu)樽远x的類或結(jié)構(gòu)體定義運(yùn)算符的行為。通過運(yùn)算符重載,我們可以對(duì)自定義類型的對(duì)象執(zhí)行類似于內(nèi)置類型的操作,使代碼更具表達(dá)力和易讀性。
在C#
中,可以對(duì)很多運(yùn)算符進(jìn)行重載,例如算術(shù)運(yùn)算符(+、-、*、/等)、關(guān)系運(yùn)算符(==、!=、>、<等)、邏輯運(yùn)算符(&&、||等)等。
以下是一個(gè)簡單的示例,展示了如何重載等號(hào)運(yùn)算符:
開始之前,先看一下傳統(tǒng)的==
運(yùn)算符,定義一個(gè)Student
類:
class Student
{
private int age;
private string name;
private long id;
public Student(int age, string name, long id)
{
this.age = age;
this.name = name;
this.id = id;
}
}
創(chuàng)建n
個(gè)實(shí)例進(jìn)行比較:
s1
和s2
一樣但是為什么不相等呢?這是因?yàn)?code>==運(yùn)算符比較的兩個(gè)變量s1
和s2
分別是存儲(chǔ)的在堆中的s1
和s2
的內(nèi)存地址,雖然內(nèi)容一樣,但是它們指向的內(nèi)存地址是不一樣,所以就出現(xiàn)了上面的輸出結(jié)果。
那如果想在判斷是否相等的時(shí)候判斷的時(shí)候比較的是存儲(chǔ)的字段而不是變量地址怎么辦呢?這個(gè)時(shí)候就可以重載==
運(yùn)算符:
internal class Student
{
private int age;
private string name;
private long id;
public Student(int age, string name, long id)
{
this.age = age;
this.name = name;
this.id = id;
}
public static bool operator==(Student a, Student b)
{
if (a.age == b.age && a.name == b.name && a.id == b.id) return true;
return false;
}
public static bool operator !=(Student a, Student b)
{
bool result = a == b; return result;
}
}
這個(gè)時(shí)候再執(zhí)行上面的比較:文章來源:http://www.zghlxwxcb.cn/news/detail-455776.html

對(duì)于其他的運(yùn)算符重載大家可自行嘗試。文章來源地址http://www.zghlxwxcb.cn/news/detail-455776.html
到了這里,關(guān)于C# 面向?qū)ο蟮奈恼戮徒榻B完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!