一、类的访问
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(),且未声明为 abstract
public 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;
else
min = 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#代码的基础。根据场景选择合适的类型,可以避免内存泄漏、提高性能并减少错误。