文章目錄
- 一、類的訪問
- 1、普通類繼承抽象類
- 2、普通類繼承抽象類,抽象類繼承接口,三者聯系
- 二、類中方法的訪問
- 2.1 抽象方法和虛方法
- 2.2 虛方法和普通方法
- **1. 調用機制**
- **2. 方法重寫**
- **3. 設計意圖**
- **4. 性能差異**
- **5. 語法對比表**
- **總結:何時使用?**
- 三、迭代器的使用
- 3.1、使用場景及示例
- 3.2、
- 四、深度復制與淺度復制
- 4.1、解析及示例
- 1. **淺度復制(Shallow Copy)**
- 2. **深度復制(Deep Copy)**
- 五、引用和值類型
- 一、值類型(Value Types)
- 二、引用類型(Reference Types)
一、類的訪問
1、普通類繼承抽象類
在C#里,普通類繼承抽象類時,有以下這些要點需要留意:
1. 必須實現所有抽象成員
抽象類中的抽象方法和屬性不具備實現代碼,繼承它的普通類得把這些抽象成員全部實現
出來。實現時,方法簽名要和抽象類中定義的保持一致,并且要用override
關鍵字。
public abstract class Shape
{public abstract double Area(); // 抽象方法
}public class Circle : Shape
{public double Radius { get; set; }// 實現抽象方法public override double Area() => Math.PI * Radius * Radius;
}
2. 遵循訪問修飾符的限制
在實現抽象成員時,訪問修飾符要和抽象類中定義的一樣。比如,抽象類里的抽象方法是protected
,那么派生類中實現該方法時也得用protected
。
3. 不能直接實例化抽象類
抽象類沒辦法直接創建實例,必須通過派生類來實例化。
Shape shape = new Circle { Radius = 5 }; // 正確
Shape shape = new Shape(); // 錯誤,無法實例化抽象類
4. 可以添加新成員
繼承抽象類的普通類能夠新增自己的字段、屬性、方法或者事件。
public class Rectangle : Shape
{public double Width { get; set; }public double Height { get; set; }public override double Area() => Width * Height;// 新增方法public double Perimeter() => 2 * (Width + Height);
}
5. 抽象類可以包含非抽象成員
抽象類中除了抽象成員,還能有已經實現的方法、屬性等,派生類可以直接繼承或者重寫這些非抽象成員。
public abstract class Animal
{public string Name { get; set; }public void Eat() => Console.WriteLine($"{Name} is eating."); // 非抽象方法public abstract void MakeSound(); // 抽象方法
}public class Dog : Animal
{public override void MakeSound() => Console.WriteLine("Woof!");
}
6. 抽象類也能繼承自其他類或抽象類
如果抽象類繼承了另一個抽象類,它可以選擇實現部分抽象成員,剩下的由派生類去實現。
public abstract class Vehicle
{public abstract void Start();
}public abstract class Car : Vehicle
{public override void Start() => Console.WriteLine("Car started."); // 實現基類的抽象方法public abstract void Drive(); // 定義新的抽象方法
}public class SportsCar : Car
{public override void Drive() => Console.WriteLine("Sports car is driving fast.");
}
7. 不能用 sealed 修飾派生類
因為普通類要實現抽象類的抽象成員,所以不能用sealed
關鍵字修飾該普通類,不然就沒辦法被其他類繼承了。
總結
普通類繼承抽象類時,要實現所有抽象成員,遵循訪問修飾符的規則,不能實例化抽象類,不過可以添加新成員。抽象類可以有非抽象成員,還能繼承其他類或抽象類。
2、普通類繼承抽象類,抽象類繼承接口,三者聯系
當一個類(派生類)繼承抽象基類,而抽象基類又實現了接口時,三者的成員函數關系遵循以下規則(以C#為例):
1. 接口定義“契約”,抽象基類部分或全部實現,派生類完成剩余實現
- 接口:定義必須實現的成員(如方法、屬性),但不提供實現。
- 抽象基類:
- 必須“聲明”實現接口的所有成員(即使只實現部分)。
- 可將部分接口成員標記為
abstract
(延遲到派生類實現),其他成員提供具體實現。
- 派生類:
- 必須實現抽象基類中標記為
abstract
的接口成員(若有)。 - 可選擇重寫(
override
)抽象基類中已實現的接口成員(若為virtual
)。
- 必須實現抽象基類中標記為
2. 示例說明
假設存在以下結構:
// 接口定義
public interface IMyInterface
{void MethodA(); // 接口方法void MethodB();
}// 抽象基類實現接口
public abstract class MyAbstractBase : IMyInterface
{public void MethodA() // 具體實現接口方法{Console.WriteLine("Base.MethodA");}public abstract void MethodB(); // 抽象方法,延遲到派生類實現
}// 派生類繼承抽象基類
public class MyDerivedClass : MyAbstractBase
{public override void MethodB() // 實現抽象基類的抽象方法{Console.WriteLine("Derived.MethodB");}
}
成員關系分析:
- 接口
IMyInterface
:定義MethodA()
和MethodB()
。 - 抽象基類
MyAbstractBase
:- 實現
MethodA()
,派生類可直接使用。 - 將
MethodB()
標記為abstract
,強制派生類實現。
- 實現
- 派生類
MyDerivedClass
:- 無需關心
MethodA()
(已由基類實現)。 - 必須實現
MethodB()
,否則會編譯錯誤。
- 無需關心
3. 特殊情況:抽象基類未完全實現接口
若抽象基類未實現接口的所有成員(即部分接口成員未被標記為 abstract
且未提供實現),則會導致編譯錯誤。例如:
public abstract class MyAbstractBase : IMyInterface
{// 錯誤:未實現 MethodB(),且未聲明為 abstractpublic void MethodA() { }
}
修正方式:
- 將
MethodB()
聲明為abstract
(如示例所示)。 - 或在抽象基類中提供
MethodB()
的具體實現。
4. 接口顯式實現與隱式實現
抽象基類可選擇顯式實現接口(只能通過接口類型調用):
public abstract class MyAbstractBase : IMyInterface
{void IMyInterface.MethodA() // 顯式實現接口方法{Console.WriteLine("Explicit implementation");}public abstract void MethodB();
}
此時,派生類需通過接口類型調用 MethodA()
:
MyDerivedClass derived = new MyDerivedClass();
((IMyInterface)derived).MethodA(); // 必須轉型為接口類型
5. 派生類重寫基類的實現
若抽象基類的方法為 virtual
,派生類可選擇重寫:
public abstract class MyAbstractBase : IMyInterface
{public virtual void MethodA() { } // 虛擬方法public abstract void MethodB();
}public class MyDerivedClass : MyAbstractBase
{public override void MethodA() { } // 重寫基類方法public override void MethodB() { } // 實現抽象方法
}
6. 多層繼承的擴展
若存在多層繼承(如抽象基類繼承自另一個抽象基類),規則相同:
- 每個抽象基類可實現部分接口成員,剩余抽象成員由最終派生類實現。
- 示例:
public interface IMyInterface { void MethodA(); } public abstract class Base1 : IMyInterface { public abstract void MethodA(); } public abstract class Base2 : Base1 { } // 未實現 MethodA(),仍為抽象類 public class Derived : Base2 { public override void MethodA() { } } // 最終實現
總結
角色 | 對接口成員的責任 | 對抽象成員的責任 |
---|---|---|
接口 | 定義所有成員簽名 | 無 |
抽象基類 | 必須聲明實現所有接口成員(部分或全部實現) | 可定義抽象成員,強制派生類實現 |
派生類 | 實現抽象基類中未實現的接口成員(即抽象成員) | 必須實現基類的所有抽象成員 |
這種分層設計允許:
- 接口 定義統一契約。
- 抽象基類 復用通用邏輯,簡化派生類實現。
- 派生類 專注于核心差異化邏輯。
二、類中方法的訪問
2.1 抽象方法和虛方法
在C#中,抽象方法和虛方法都用于實現多態性,但它們的設計目的和使用方式有本質區別。以下是兩者的核心差異:
1. 定義語法與強制實現
抽象方法 | 虛方法 |
---|---|
使用 abstract 關鍵字聲明,且不能有方法體。csharp<br>public abstract void Print();<br> | 使用 virtual 關鍵字聲明,必須有默認實現。csharp<br>public virtual void Print() { Console.WriteLine("Base"); }<br> |
必須由派生類實現,否則派生類必須聲明為抽象類。 | 派生類可以選擇是否重寫,不重寫時將繼承基類的默認實現。 |
2. 所在類的限制
- 抽象方法:只能存在于抽象類中(即使用
abstract
修飾的類)。 - 虛方法:可以存在于普通類或抽象類中。
3. 重寫要求
抽象方法 | 虛方法 |
---|---|
派生類必須使用 override 關鍵字實現,且不能使用 new 或 sealed 隱藏基類方法。 | 派生類使用 override 關鍵字重寫(推薦),或使用 new 關鍵字隱藏基類方法(不推薦)。 |
示例:csharp<br>public override void Print() { ... }<br> | 示例:csharp<br>public override void Print() { ... } // 重寫<br>public new void Print() { ... } // 隱藏(不推薦)<br> |
4. 設計目的
- 抽象方法:用于定義必須由子類實現的契約,基類只規定方法簽名,不提供默認行為。例如:
public abstract class Shape {public abstract double Area(); // 所有形狀必須計算面積 }
- 虛方法:用于提供可擴展的默認行為,允許子類在需要時修改實現。例如:
public class Animal {public virtual void Speak() { Console.WriteLine("Animal sound"); } }public class Dog : Animal {public override void Speak() { Console.WriteLine("Woof"); } // 可選重寫 }
5. 調用方式
- 抽象方法:無法直接調用,必須通過派生類的實現調用。
- 虛方法:可以直接通過基類調用默認實現,也可以通過派生類調用重寫后的實現。
總結對比表
特性 | 抽象方法 | 虛方法 |
---|---|---|
方法體 | 不能有方法體 | 必須有默認實現 |
所在類 | 必須在抽象類中 | 可以在普通類或抽象類中 |
強制實現 | 派生類必須實現 | 派生類可選重寫 |
關鍵字 | abstract + override | virtual + override (可選) |
設計意圖 | 定義必須實現的契約 | 提供可擴展的默認行為 |
示例代碼
// 抽象類 + 抽象方法
public abstract class Vehicle
{public abstract void Start(); // 必須由子類實現
}// 普通類 + 虛方法
public class Car : Vehicle
{public override void Start() { Console.WriteLine("Car started"); } // 實現抽象方法public virtual void Drive() { Console.WriteLine("Driving normally"); } // 虛方法,提供默認行為
}// 派生類重寫虛方法
public class SportsCar : Car
{public override void Drive() { Console.WriteLine("Driving fast!"); } // 重寫虛方法
}
何時使用?
- 使用抽象方法:當基類無法提供有意義的默認實現,且所有子類必須強制實現某個行為時。
- 使用虛方法:當基類可以提供默認行為,但子類可能需要自定義實現時。
通過合理使用抽象方法和虛方法,可以構建出靈活且易于擴展的面向對象系統。
2.2 虛方法和普通方法
在C#中,虛方法(virtual
)和普通方法(無修飾符)的核心區別在于是否支持運行時多態。以下是兩者的詳細對比:
1. 調用機制
虛方法 | 普通方法 |
---|---|
使用 virtual 關鍵字聲明,支持運行時多態。基類的虛方法可以在派生類中被override 重寫。調用時,會根據對象的實際類型決定執行哪個版本的方法。 | 沒有特殊修飾符,不支持運行時多態。調用時,根據對象的聲明類型決定執行的方法,無論對象的實際類型是什么。 |
示例:csharp<br>public class Animal {<br> public virtual void Speak() { Console.WriteLine("Animal"); }<br>}<br><br>public class Dog : Animal {<br> public override void Speak() { Console.WriteLine("Dog"); }<br>}<br><br>// 輸出:Dog<br>Animal animal = new Dog();<br>animal.Speak(); // 調用Dog的實現<br> | 示例:csharp<br>public class Animal {<br> public void Speak() { Console.WriteLine("Animal"); }<br>}<br><br>public class Dog : Animal {<br> public new void Speak() { Console.WriteLine("Dog"); } // 使用new隱藏基類方法(不推薦)<br>}<br><br>// 輸出:Animal<br>Animal animal = new Dog();<br>animal.Speak(); // 調用Animal的實現<br> |
2. 方法重寫
虛方法 | 普通方法 |
---|---|
可以被派生類使用 override 關鍵字重寫,從而改變方法的行為。 | 不能被重寫,但可以使用 new 關鍵字隱藏基類方法(但這不是真正的重寫,只是創建了一個同名的新方法)。 |
正確做法:csharp<br>public class Base {<br> public virtual void Print() { ... }<br>}<br><br>public class Derived : Base {<br> public override void Print() { ... } // 重寫虛方法<br>}<br> | 錯誤做法(隱藏而非重寫):csharp<br>public class Base {<br> public void Print() { ... }<br>}<br><br>public class Derived : Base {<br> public new void Print() { ... } // 隱藏基類方法(編譯警告)<br>}<br> |
3. 設計意圖
虛方法 | 普通方法 |
---|---|
用于實現多態性,允許基類定義通用行為,派生類根據需要自定義實現。例如:csharp<br>public class Shape {<br> public virtual double Area() => 0;<br>}<br><br>public class Circle : Shape {<br> public override double Area() => Math.PI * Radius * Radius;<br>}<br> | 用于實現固定行為,不希望派生類修改方法邏輯。例如:csharp<br>public class Calculator {<br> public int Add(int a, int b) => a + b; // 不需要重寫的方法<br>}<br> |
4. 性能差異
- 虛方法:調用時需要通過虛函數表(VTable)動態查找實際要執行的方法,因此性能略低(但在大多數場景下可以忽略不計)。
- 普通方法:調用時直接綁定到聲明類型的方法,性能更高。
5. 語法對比表
特性 | 虛方法 | 普通方法 |
---|---|---|
關鍵字 | virtual | 無 |
能否重寫 | 能(使用 override ) | 不能(只能用 new 隱藏) |
多態支持 | 運行時多態(根據對象實際類型) | 編譯時綁定(根據聲明類型) |
默認行為 | 基類提供默認實現,可被覆蓋 | 行為固定,不可被派生類修改 |
性能 | 略低(通過VTable查找) | 更高(直接調用) |
總結:何時使用?
- 使用虛方法:
- 當基類希望派生類能夠自定義某個方法的實現時。
- 需要通過基類引用調用派生類方法(實現多態)。
- 使用普通方法:
- 當方法的邏輯不需要被派生類修改時。
- 性能敏感的場景(如高頻調用的方法)。
通過合理使用虛方法和普通方法,可以在保證代碼靈活性的同時,避免不必要的性能開銷。
三、迭代器的使用
3.1、使用場景及示例
在迭代塊中,使用yield關鍵字選擇要在foreach循環中使用的值,其語法如下
yield return <value>;
- 迭代一個類成員(比如方法)
IEnumerable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace SimpleIterators
{class Program{public static IEnumerable SimpleList(){yield return "string 1";yield return "string 2";yield return "string 3";}static void Main(string[] args){foreach (string item in SimpleList())Console.WriteLine(item);Console.ReadKey();}}
}
輸出
- 迭代一個類
Enumerator
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace Ch11Ex03
{public class Primes{private long min;private long max;public Primes(): this(2, 100){}public Primes(long minimum, long maximum){if (minimum < 2)min = 2;elsemin = minimum;max = maximum;}public IEnumerator GetEnumerator(){for (long possiblePrime = min; possiblePrime <= max; possiblePrime++){bool isPrime = true;for (long possibleFactor = 2; possibleFactor <=(long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++){long remainderAfterDivision = possiblePrime % possibleFactor;if (remainderAfterDivision == 0){isPrime = false;break;}}if (isPrime){yield return possiblePrime;}}}}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace Ch11Ex03
{class Program{static void Main(string[] args){Primes primesFrom2To1000 = new Primes(2, 1000);//Primes primesFrom2To1000 = new Primes( );foreach (long i in primesFrom2To1000)Console.Write("{0} ", i);Console.ReadKey();}}
}
primesFrom2To1000 為定義的一個類,在foreach里迭代這個類,
yield return possiblePrime;
為迭代要輸出的值,輸出的值的類型為定義的 long possiblePrime
輸出如下
3.2、
四、深度復制與淺度復制
4.1、解析及示例
在C#中,深度復制(Deep Copy)和淺度復制(Shallow Copy)是處理對象復制時的兩種不同方式,它們的核心區別在于是否遞歸復制對象的所有成員。以下是詳細解釋和示例:
1. 淺度復制(Shallow Copy)
- 定義:創建一個新對象,但只復制對象的頂層成員。對于引用類型的成員,只復制引用(內存地址),而不復制實際對象。
- 特點:
- 新對象和原對象是不同的實例(內存地址不同)。
- 引用類型的成員指向同一個對象。
- 修改引用類型成員會影響所有關聯的對象。
- 實現方式:
- 使用
MemberwiseClone()
方法(受保護,需在類內部實現)。 - 手動復制每個字段。
- 使用
示例代碼
public class Address
{public string City { get; set; }
}public class Person
{public string Name { get; set; } // 值類型public Address Address { get; set; } // 引用類型// 實現淺復制方法public Person ShallowCopy(){return (Person)this.MemberwiseClone();}
}// 使用示例
Person original = new Person
{Name = "張三",Address = new Address { City = "北京" }
};Person shallowCopy = original.ShallowCopy();// 修改淺復制對象的引用類型成員
shallowCopy.Address.City = "上海";Console.WriteLine(original.Address.City); // 輸出: 上海(被修改了)
2. 深度復制(Deep Copy)
- 定義:創建一個新對象,并遞歸復制對象的所有成員。對于引用類型的成員,會創建新的對象實例,而非僅復制引用。
- 特點:
- 新對象和原對象完全獨立,沒有共享的引用類型成員。
- 修改任何一個對象都不會影響其他對象。
- 實現方式:
- 手動遞歸復制每個引用類型成員。
- 使用序列化和反序列化(需類標記為
[Serializable]
)。
示例代碼(手動實現)
public class Address
{public string City { get; set; }// 提供深度復制方法public Address DeepCopy(){return new Address { City = this.City };}
}public class Person
{public string Name { get; set; }public Address Address { get; set; }// 實現深度復制方法public Person DeepCopy(){return new Person{Name = this.Name,Address = this.Address.DeepCopy() // 遞歸復制引用類型};}
}// 使用示例
Person original = new Person
{Name = "張三",Address = new Address { City = "北京" }
};Person deepCopy = original.DeepCopy();// 修改深度復制對象的引用類型成員
deepCopy.Address.City = "上海";Console.WriteLine(original.Address.City); // 輸出: 北京(未被修改)
示例代碼(使用序列化)
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;[Serializable] // 必須標記為可序列化
public class Address
{public string City { get; set; }
}[Serializable]
public class Person
{public string Name { get; set; }public Address Address { get; set; }// 使用序列化實現深度復制public Person DeepCopy(){using (MemoryStream stream = new MemoryStream()){BinaryFormatter formatter = new BinaryFormatter();formatter.Serialize(stream, this);stream.Position = 0;return (Person)formatter.Deserialize(stream);}}
}
-
對比表格
| 特性 | 淺度復制 | 深度復制 |
|------------------------|------------------------------|------------------------------|
| 新對象實例 | 創建頂層對象 | 創建所有層級的對象 |
| 引用類型成員 | 共享同一個實例 | 創建新實例 |
| 值類型成員 | 復制值 | 復制值 |
| 實現復雜度 | 低(使用MemberwiseClone
) | 高(遞歸或序列化) |
| 修改影響 | 影響所有共享引用的對象 | 僅影響當前對象 |
| 性能 | 高(僅復制引用) | 低(需創建多個對象) | -
常見問題
- 循環引用:深度復制時需小心處理循環引用,可能導致棧溢出。
- 不可序列化類型:使用序列化方法時,所有成員必須可序列化。
- 性能開銷:深度復制涉及創建多個對象,對性能有影響。
- 選擇建議
- 使用淺度復制:當對象的引用類型成員是不可變的,或不需要獨立修改時。
- 使用深度復制:當需要完全獨立的對象,避免修改相互影響時。
總結
- 淺度復制:復制頂層對象,共享引用類型成員。
- 深度復制:遞歸復制所有成員,創建完全獨立的對象。
理解這兩種復制方式的區別,有助于避免在代碼中出現意外的副作用,并根據需求選擇合適的復制策略。
五、引用和值類型
在C#中,變量類型分為值類型和引用類型,它們在內存存儲、傳遞方式和生命周期等方面有本質區別。以下是常見的值類型和引用類型及其特點:
一、值類型(Value Types)
值類型變量直接存儲數據值,通常分配在棧(Stack)上(局部變量)或結構體中。值類型的復制會創建獨立的副本。
- 內置值類型
分類 | 類型 | 示例 |
---|---|---|
整數 | byte , sbyte , short , ushort , int , uint , long , ulong | int age = 30; |
浮點數 | float , double , decimal | double price = 9.99; |
布爾 | bool | bool isActive = true; |
字符 | char | char letter = 'A'; |
枚舉 | enum (自定義) | enum Color { Red, Green, Blue }; |
元組 | (int, string) (C# 7.0+) | var person = (1, "Alice"); |
- 結構體(Struct)
結構體是用戶自定義的值類型,常用于輕量級數據存儲:
public struct Point
{public int X;public int Y;
}Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // 復制值,p2與p1獨立
- 可空值類型(Nullable)
允許值類型變量存儲null
:
int? nullableInt = null; // 可空整數
bool? nullableBool = false;
二、引用類型(Reference Types)
引用類型變量存儲對象的內存地址(引用),對象本身分配在堆(Heap)上。引用類型的復制僅傳遞引用,多個變量可能指向同一對象。
- 內置引用類型
分類 | 類型 | 示例 |
---|---|---|
字符串 | string | string name = "John"; |
數組 | T[] (任意類型的數組) | int[] numbers = new int[5]; |
集合 | List<T> , Dictionary<TKey, TValue> , HashSet<T> 等 | List<string> names = new List<string>(); |
- 類(Class)
類是最常見的引用類型,包括自定義類和框架類:
public class Person
{public string Name { get; set; }
}Person p1 = new Person { Name = "Alice" };
Person p2 = p1; // 復制引用,p2和p1指向同一對象
- 接口(Interface)
接口本身不能實例化,但實現接口的類是引用類型:
public interface IAnimal
{void Speak();
}public class Dog : IAnimal
{public void Speak() => Console.WriteLine("Woof!");
}IAnimal animal = new Dog(); // 引用類型
- 委托(Delegate)
委托是方法的類型安全引用,屬于引用類型:
public delegate void MyDelegate(string message);MyDelegate del = Console.WriteLine; // 委托實例
- 對象(Object)
所有類型的基類,可引用任何類型的對象:
object obj = "Hello"; // 引用字符串對象
obj = 123; // 引用整數對象(裝箱)
- 動態類型(Dynamic)
在運行時確定類型,屬于引用類型:
dynamic dynamicVar = "Hello";
dynamicVar = 123; // 運行時有效
三、關鍵區別總結
特性 | 值類型 | 引用類型 |
---|---|---|
存儲位置 | 棧或結構體 | 堆 |
復制方式 | 創建獨立副本 | 復制引用(共享對象) |
默認值 | 0 , false , null (可空類型) | null |
基類 | System.ValueType | System.Object |
常見類型 | 基本數據類型、結構體、枚舉 | 類、接口、數組、字符串、委托 |
四、特殊注意事項
-
字符串的不可變性:
string
是引用類型,但由于不可變性,賦值時看似創建了副本:string a = "Hello"; string b = a; // 復制引用,但字符串不可變 b = "World"; // b指向新字符串,a不受影響
-
裝箱與拆箱:值類型與
object
之間的轉換會產生性能開銷:int num = 100; object boxed = num; // 裝箱(值類型→引用類型) int unboxed = (int)boxed; // 拆箱(引用類型→值類型)
-
結構體與類的選擇:
- 結構體:輕量級、頻繁創建/銷毀、數據獨立。
- 類:復雜行為、需要繼承、共享狀態。
理解值類型和引用類型的區別是編寫高效、安全C#代碼的基礎。根據場景選擇合適的類型,可以避免內存泄漏、提高性能并減少錯誤。