1.什么是虚方法?它和抽象方法有什么不同?
共同点:都支持多态、在子类中都可以进行重写、必须在类的内部(不能是static的)
不同点:
对比项 | virtual 虚方法 | abstract 抽象方法 |
---|---|---|
是否有默认实现 | ✅ 有默认实现。 | ❌ 没有实现,派生类必须实现。 |
是否必须被重写 | ❌ 可以不重写,派生类也能继承使用。 | ✅ 必须重写,否则编译报错。 |
所在类是否可以实例化 | ✅ 所在类可以实例化(非抽象类)。 | ❌ 所在类必须是抽象类,不能实例化。 |
用途场景 | 提供一个可被覆盖但非必须覆盖的方法。 | 强制派生类提供特定行为的实现。 |
public abstract class Animal
{
public abstract void Speak();
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof");
}
}
抽象方法 Speak()
没有实现,所有非抽象派生类必须重写它。
2.接口可以作为参数传递给函数吗
是的,接口在C#中完全可以作为参数传递给函数。这是接口最常见和最强大的用途之一:它允许你传入任何实现了该接口的对象,实现 松耦合 和 多态性。
public interface ILogger {
void Log(string message);
}
public class ConsoleLogger : ILogger {
public void Log(string message) {
Console.WriteLine("Console: " + message);
}
}
public class FileLogger : ILogger {
public void Log(string message) {
// 假设写入文件
Console.WriteLine("File: " + message);
}
}
public class Processor {
public void Process(ILogger logger) {
logger.Log("Processing started...");
}
}
class Program {
static void Main() {
Processor p = new Processor();
// 传入 ConsoleLogger 实例
p.Process(new ConsoleLogger()); // 输出:Console: Processing started...
// 传入 FileLogger 实例
p.Process(new FileLogger()); // 输出:File: Processing started...
}
}
注:接口的用法:通过实现接口的类来使用接口方法
你必须创建一个实现了该接口的类的实例,然后可以通过该类对象(或接口引用)来调用接口中定义的方法。
3.关键字区别:abstract和virtual
在抽象类中:
你想要的行为 | 用法 |
---|---|
让子类必须实现这个方法 | 用 abstract |
提供一个默认实现,让子类可以选择是否重写 | 用 virtual |
不想让子类改这个方法 | 普通方法(不加任何关键字) |
4.Lambda表达式
4.1 语法:(参数) => { 语句块 }
例1:x => x * x 等效于 (int x) => { return x * x; }
例2:两个参数的lambda表达式 (x, y) => x + y
4.2 委托和lambda表达式的配合:
4.2.1 委托的定义:委托可以理解为 函数的引用类型。它可以存储对方法的引用,就像变量可以存储数值一样。
// 定义一个接收int,返回int的委托类型
public delegate int MyDelegate(int x);
// 目标方法
public static int Square(int x) => x * x;
public static void Main()
{
// 创建委托实例,指向 Square 方法
MyDelegate del = Square;
int result = del(5); // 调用委托
Console.WriteLine(result); // 输出 25
}
上面的int Square(int x) => x * x是表达式体成员(Expression-bodied member),等价于:
public static int Square(int x)
{
return x * x;
}
4.2.2 委托+lambda表达式例子
MyDelegate del = x => x * x;
Console.WriteLine(del(6)); // 输出 36
4.3 常用的内置委托类型
委托类型 | 用法说明 |
---|---|
Action | 无返回值 |
Func<T> | 有返回值(最后一个类型是返回类型) |
Predicate<T> | 返回 bool 的函数 |
Func<>
是 C# 提供的 内置委托类型,用于表示:有返回值的方法或 lambda 表达式。
语法: Func<参数类型1, 参数类型2, …, 返回值类型> 变量名 = lambda表达式或方法;
例:两个参数,一个返回值
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3)); // 输出 5
5.匿名方法和lambda表达式的关系
在 C# 中,匿名方法(Anonymous Methods) 是一种没有名称的方法,通常用于快速定义委托的内容,特别是在需要临时、内联逻辑的场景中。匿名方法是在 C# 2.0 中引入的,后来被 Lambda 表达式(C# 3.0) 所取代或简化。
也就是说,lambda是一种简洁地表示匿名方法的语法。
// 定义一个委托类型
delegate int MyDelegate(int x, int y);
class Program
{
static void Main()
{
// 使用匿名方法创建委托实例
MyDelegate add = delegate(int x, int y)
{
return x + y;
};
Console.WriteLine(add(3, 4)); // 输出 7
}
}
Func<int, int> anon = delegate(int i)
{
i = i+1;
return i;
};
//输出2
Console.WriteLine(anon(1));
6. 什么是闭包
闭包是指 Lambda 表达式或匿名方法中引用了外部作用域中的变量,这些变量会随着 Lambda 一起“打包”进委托中,延长其生命周期。
public static Func<int, int> CreateAdder(int y)
{
return x => x + y; // y 是外部变量,被闭包捕获
}
var add5 = CreateAdder(5);
Console.WriteLine(add5(10)); // 输出 15
这里的 y
虽然定义在 CreateAdder
方法中,但在 Lambda 中被使用,所以会被“捕获”,即便方法返回之后,y
仍然存在。
例子:
List<Func<int>> actions = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => i);
}
foreach (var act in actions)
Console.WriteLine(act()); // 输出什么?
解析:lambda表达式 () => i
捕获了变量 i
,而不是 i
的值。由于 i
是 for
循环的迭代变量,它在循环中是同一个变量(每次迭代会更新值),因此所有lambda表达式都捕获了同一个 i
。
它最后值变为 3,所以每个 lambda 都输出 3。
7.回顾:重载和重写的区别
1. 作用域
- 重载(Overload):
发生在同一作用域内(如同一个类中或全局作用域)。 - 重写(Override):
发生在继承关系中,派生类对基类的虚函数进行重新定义。
2. 函数签名
- 重载(Overload):
- 函数名相同,但参数列表必须不同(参数类型、个数或顺序不同)。
- 返回类型可以不同,但仅返回类型不同不足以构成重载。
void print(int a); // 合法重载
void print(double a); // 合法重载
int print(int a); // 错误:仅返回类型不同,无法重载
- 重写(Override):
- 函数名、参数列表、返回类型必须与基类虚函数严格一致(协变返回类型除外)。
- 基类函数必须声明为
virtual
,派生类函数可以用override
显式标记。
class Base {
public:
virtual void show() { cout << "Base"; }
virtual Base* clone() { return new Base(); }
};
class Derived : public Base {
public:
void show() override { cout << "Derived"; } // 合法重写
Derived* clone() override { return new Derived(); } // 协变返回类型
};
3. 多态性
- 重载(Overload):
属于静态多态(编译时多态),编译器根据调用时的参数决定具体函数。
obj.print(5); // 调用 print(int)
obj.print(3.14); // 调用 print(double)
- 重写(Override):
属于动态多态(运行时多态),通过虚函数表和基类指针/引用实现动态绑定。
Base* ptr = new Derived();
ptr->show(); // 输出 "Derived"
4. 其他关键点
- 虚函数要求:
- 重写必须基于基类的虚函数(
virtual
)。 - 重载与虚函数无关,可以是普通函数或虚函数。
- 重写必须基于基类的虚函数(
- const 限定符:
- 成员函数的
const
版本和非const
版本可构成重载。 - 重写时,
const
限定符必须一致。
- 成员函数的
void func() const; // 重载:const 版本
void func(); // 非 const 版本
- 隐藏(Hiding):
若派生类定义了与基类同名但参数不同的函数,会隐藏基类函数(需用using
或作用域运算符访问)。
class Base { public: void func(int); };
class Derived : public Base { public: void func(double); };
Derived d;
d.func(5); // 调用 Derived::func(double),Base::func(int) 被隐藏
d.Base::func(5); // 显式调用基类版本
特性 | 重载(Overload) | 重写(Override) |
---|---|---|
作用域 | 同一作用域 | 基类与派生类之间 |
函数签名 | 参数列表不同 | 函数名、参数、返回类型一致 |
多态类型 | 静态多态(编译时) | 动态多态(运行时) |
虚函数要求 | 无关 | 必须基于基类虚函数 |
关键字 | 无 | virtual (基类)、override (派生类) |
典型用途 | 扩展同名函数功能 | 实现多态性,定制派生类行为 |