接口、抽象类、类的概述及区别、使用场景
1.类(class):
- 类是面向对象编程的基本组成单元,用于描述属性和行为的对象集合
- 类可以包含属性(也成字段或成员变量)和方法(也称函数或成员函数)
- 类可以被实例化为对象,每个对象都有自己的状态(属性值)和行为(方法)
- 类可以被继承,也可以继承其他类,但不能继承多个类(父类只能有一个)。
2.抽象类(Abstract Class):
- 抽象类是不能被实例化的类,他存在的意义在于作为其他类的基类,用于继承和重用
- 抽象类可以包含抽象方法,这些放大只有声明而没有具体的实现
- 子类必须实现抽象类中的所有抽象方法,否则子类也必须声明为抽象类
3.接口(Interface):
- 接口定义了一组方法、字段的契约,单不包含具体实现。
- 类可以实现一个或多个接口,并提供接口中定义的方法的实现
- 接口提供了一种方式来实现多态性,既一个对象可以被看作是他实现的任何接口类型
区别
- 抽象类可以包含方法的实现、二接口只能包含方法的声明
- 类可以实现多个接口,但只能继承一个抽象类
- 抽象类可以包含多个抽象方法,接口不能包含非抽象方法,接口只能声明public的方法
- 抽象类的目的是为了实现代码重用和提供一种约束,而接口的目的是为了实现多态性和实现类之间的松耦关系
- 抽象类是接口与类的中介。
使用场景
- 当多个类具有共同的行为,但每个类的实现细节可能不同时,可以使用抽象类
- 当需要定义一组类共同遵循的契约,但这些类可能来自不同的继承体系时,可以使用接口
- 接口通常用于定义与特定领域无关的行为,而抽象类通常用于定义特定领域的通用行为
组合类和继承的优缺点及使用场景
继承:
优点:
- 代码重用:之类可以继承父类的属性和方法,避免了重复编写相似的代码
- 扩展性:通过扩展已有来创建新类,可以快速实现新功能
缺点
- 耦合度高:破坏封装,子类和父类存在紧密的关联,子类的修改可能会影响到父类和其他之类
- 继承层次复杂:过深或过复杂的继承层次会增加代码的复杂性和理解难度
- 局限性:只支持单继承
适用场景:
- 当子类是父类的一种特殊类型,并且可以继承父类的大部分属性和方法时,适合适用继承
组合:
优点:
- 松耦合:组合将类之间的关系降低到一个松耦合的成都,一个类的变化不会对另一个类造成影响
- 灵活性:通过组合,可以动态的在运行时改变对象之间的关系,更容易实现复杂的行为
- 复用性:组合将已有的类组合创建新的类,对局部类进行包装、封装,提供新的接口,提高了代码的可重用性
缺点:
- 代码量增加:组合可能导致更多的类和对象,增加代码的复杂度
- 设计难度:设计良好的组合关系需要考虑更多的因素,可能增加设计和实现的复杂性
- 性能:创建组合类是,需要创建所有的局部类的对象,性能增加
适应场景:
- 当一个类需要使用另外一个类的功能,但不需要继承其所有的功能,适合使用组合
- 当需要在运行时动态的改变对象之间的关系,或实现一种松耦合的设计时,可选择组合
析构函数是什么?,析构函数什么时候调用
析构函数是一个特殊的方法,用于在对象被销毁时执行清理操作。方法名字和类名相同,但前面有一个波浪号(~)
调用:
- 当对象实例被销毁时,也就是当对象的生命周期结束时。这通常发生在程序结束时,或当对象所在的作用域结束时(例如,他是一个局部变量)
- GC释放对象时,如果对象处于不在使用的状态,垃圾回收期可能会调用析构函数来进行额外的清理操作
- 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
注:c#的析构函数通常用于释放非托管资源,比如打开的文件,数据库连接或者其他外部资源,c#资源释放,推荐使用‘IDisposable’,因为这种方式更加可控,而且不依赖垃圾回收器的工作
类怎么实现两个包含相同方法签名的接口
当类需要实现两个包含相同方法签名的接口时,需要在类中显示的实现这个方法,已满与接口的要求
using System;
// 定义第一个接口
interface IInterface1
{
void MyMethod();
}
// 定义第二个接口
interface IInterface2
{
void MyMethod();
}
// 实现类同时继承两个接口
class MyClass : IInterface1, IInterface2
{
// 显式实现接口的方法
void IInterface1.MyMethod()
{
Console.WriteLine("Implementation of MyMethod from IInterface1");
}
// 显式实现接口的方法
void IInterface2.MyMethod()
{
Console.WriteLine("Implementation of MyMethod from IInterface2");
}
}
class Program
{
static void Main(string[] args)
{
// 创建 MyClass 实例
MyClass obj = new MyClass();
// 通过接口调用 MyMethod
((IInterface1)obj).MyMethod(); // 调用来自 IInterface1 的实现
((IInterface2)obj).MyMethod(); // 调用来自 IInterface2 的实现
}
}
什么是虚方法,他和抽象方法有什么不同
虚方法:虚方法是指在基类中声明的方法,可以被子类继承并重写,在面向对象的继承关系中,通过虚方法可以实现多态性,既同一个方法名可以在不同的子类中表现出不同的行为。虚方法通过在方法声明前加上关键字‘virtual
’来定义。
抽象方法:抽象方法时一种在基类中声明但不进行实现的方法,只包含方法签名而不包含方法体。抽象方法必须在子类中被重写才能给我具体的实现。抽象方法通过在方法声明前加上关键字‘abstract’来定义抽象,且包含抽象方法的类必须抽象类。抽象类本身不能被实例化,只能被继承。
区别:
- 虚方法可以在基类中有默认的实现,子类可以选择性覆盖。而抽象方法必须在子类进行具体的实现,否则子类也必须声明为抽象类
- 虚方法通常用于允许子类修改或扩展父类的行为,而抽象方法通常用于定义一种接口,要求所有子类必须实现这个发方法。
方法重载和方法覆盖
方法重载和覆盖都是一种多态性
- 方法重载是指我们有一个名称相同但签名不同的函数
- 方法覆盖是当我们使用override 关键字覆盖子类中基类的虚方法或抽象方法时
static关键字
‘static’关键字声明静态成员或静态类 。使用 static 修饰符可声明属于类型本身而不是属于特定对象的静态成员。static 修饰符可用于声明 static 类。在类、接口和结构中,可以将 static 修饰符添加到字段、方法、属性、运算符、事件和构造函数。static 修饰符不能用于索引器或终结器。
静态成员:
在类中声明的静态成员是与类本身相关联的,而不是与类的实例关联。这意味着无需创建类的实例就可以访问静态成员,通过类名直接访问即可。
- 静态字段:静态字段在整个应用程序中只有一个副本,所有类的实例共享同一个静态字段的值
- 静态方法:静态方法不需要通过类实例化来调用,可以直接使用类名调用。可用于辅助方法,工具方法等
- 静态属性:静态属性可用于获取或设置与类相关但不依赖与特定实例的值。
静态类:
静态类是一种特殊的;类,他只能包含静态成员,且不能被实例化。静态类在应用程序启动时被阿吉仔到内存中,并且其中的静态成员在整个应用程序生命周期内可用。
static使用场景:
- 工具方法:如数学计算函数,字符串处理
- 共享数据:如全局配置信息,计数器
- 单例模式:
readonly 关键字
readonly用于声明制度字段。只读字段是指其值只能在声明时或在构造中进行初始化,一单迟淑华完成,就不能修改。
使用场景:
- 常量字段:如果你希望在类中定义一个常量字段,但又不想用‘const’关键字(‘const’只能用于编译是常量),可以使用‘readonly’
- 线程安全:在多线程环境中,使用‘readonly’ 可以提高字段的线程安全性。因为‘readonly’字段在初始化后不可修改,所以不会出现多线程同时修改字段值的情况。注:对于值类型(如int,bool等)或不可变的引用类型(如string),readonly是绝对线程安全的。但是对于可变的引用类型(如list,dictionary或自定义类),虽然无法改变readonly字段本身引用的对象,但仍可以修改改对象的内部状态,可能遇到线程安全问题,如向list添加元素。
public class MyClass { public readonly List<int> MyList = new List<int>(); // 可变引用类型 // ...其他代码... public void AddItem(int item) { // 需要保证线程安全,因为MyList是可变的 lock (MyList) { MyList.Add(item); } } }
static 和readonly 联合使用的好处
-
应用程序运行中保持唯一且不可修改的字段
virtual关键字
virtual 关键字用于修改方法、属性、索引器或事件声明,并使用他们可以在派生类中被重写(使用override关键字对虚方法重写)。示例:
// 基类虚方法声明
class BaseClass
{
public virtual void Method1()
{
Console.WriteLine("Base - Method1");
}
public virtual void Method2()
{
Console.WriteLine("Base - Method2");
}
}
class DerivedClass : BaseClass
{
// 重写基类中的虚方法
public override void Method1()
{
Console.WriteLine("Derived - Method1");
}
public new void Method2()
{
Console.WriteLine("Derived - Method2");
}
}
在这个示例中,DerivedClass
中的 Method2
使用了 new
关键字隐藏了 BaseClass
中的同名方法。Method1使用override 重写父类方法。
sizeof 关键字
sizeof是一个运算符而不是关键字。它用于计算特定类型或未知类型的大小(以字节为单位),通常用在编写低级代码时确定数据结构的大小。
sizeof
运算符的语法是:
sizeof(type)
其中 type
可以是任何值类型、指针类型或未知类型。
注意事项:
sizeof
运算符只能用于值类型、指针类型和未知类型。他不能用于引用类型。sizeof
运算符在编译时求值,而不是运行时,因此,他只能用于静态常量表达式。sizeof
运算符返回的类型的大小,以字节为单位。
示例:
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Size of int: " + sizeof(int)); // 输出:4(在大多数系统上)
Console.WriteLine("Size of char: " + sizeof(char)); // 输出:2(在大多数系统上)
Console.WriteLine("Size of double: " + sizeof(double)); // 输出:8(在大多数系统上)
// 通过指针类型计算大小
Console.WriteLine("Size of int* (pointer): " + sizeof(int*)); // 输出:8(在64位系统上)
}
}
in 关键字
in 关键字用于参数传递时,将参数按只读引用传递。他高速编译器在方法调用过程中不会修改改参数的值,并且可以通过引用传递避免对参数进行复制。对于大型结构或对象参数非常有用,因为直接引用参数可以提高性能的内存效率。
示例:
class Program
{
static void Main(string[] args)
{
int x = 5;
MultiplyByTwo(in x);
Console.WriteLine(x); // 输出 5
}
static void MultiplyByTwo(in int number)
{
// 无法修改 in 参数的值
// number *= 2; // 编译错误
// 仅能读取 in 参数的值
Console.WriteLine(number * 2); // 输出 10
}
}
注:in 可用于协变的定义
ref关键字
- 参数在使用ref关键字进行引用传递时,必须在方法调用之前对其初始化。
- 既可以在进入方法之前初始化参数的值,也可以在方法内部对参数进行修改。
- 如果方法需要对原始值类型变量进行修改,就需要使用ref关键来修饰对应的参数
public void testMethod() { int aa = 1; Method1(aa); Console.WriteLine($"aa: {aa}");//输出 aa:1 Method2(ref aa); Console.WriteLine($"aa: {aa}");//输出 aa:5 } public void Method1(int a) { a = 2; Console.WriteLine("Derived - Method1"); } public void Method2(ref int a) { a = 5; Console.WriteLine("Derived - Method2"); }
out 关键字
- 参数在使用out关键字进行引用传递时,不需要在方法调用前进行初始化。
- out通常用于表示方法返回多个值的情况。
- out参数必须在方法内部进行初始化,并确保在方法结束前完成复制这操作。方法内部没有为out参数赋值的情况下,方法调用将会导致编译错误。
不能将 in、ref 和 out 关键字用于以下几种方法
- 异步方法,通过使用async 修饰符定义。
- 迭代器方法,包括 yield return 或 yield break 语句。
- 扩展方法的第一个参数,其中该参数是泛型类型(即使该类型被约束为结构。)
as和is的区别
-
is 只是做类型兼容判断,并不执行真正的类型转换。返回true或false,不会返回null,对象为null也会返回false。
-
as运算符将表达式结果显式转换为给定的引用类型或可以为null值的类型。如果无法进行转换,则as运算符返回 null。
总结: as模式的效率要比is模式的高,因为借助is进行类型转换的化,需要执行两次类型兼容检查。而as只需要做一次类型兼容,一次null检查,null检查要比类型兼容检查快。
new关键字的作用
-
运算符:创建类型的新实例
-
修饰符:可以显式隐藏从基类继承的成员。
-
泛型约束:泛型约束定义,约束可使用的泛型类型。
volatile 关键字
volatile 关键字用于声明字段,以指示编译器应该每次访问字段是从内存中读取其最新值,而不是使用缓存的值。他确保了字段的可见性,但不提供原子性或互斥性。
示例:
public class Worker
{
private bool _shouldStop;
public void DoWork()
{
bool work = false;
// 注意:这里会被编译器优化为 while(true)
while (!_shouldStop)
{
work = !work; // do sth.
}
Console.WriteLine("工作线程:正在终止...");
}
public void RequestStop()
{
_shouldStop = true;
}
}
public class Program
{
public static void Main()
{
var worker = new Worker();
Console.WriteLine("主线程:启动工作线程...");
var workerTask = Task.Run(worker.DoWork);
// 等待 500 毫秒以确保工作线程已在执行
Thread.Sleep(500);
Console.WriteLine("主线程:请求终止工作线程...");
worker.RequestStop();
// 待待工作线程执行结束
workerTask.Wait();
//workerThread.Join();
Console.WriteLine("主线程:工作线程已终止");
}
}
在这个例子中,while (!_shouldStop)
会被编译器优化为 while(true)
。我们可以看一下实际的运行效果来验证这一点。切换 Release 模式,按 Ctrl + F5 运行程序,运行效果始终如下:
程序运行后,虽然主线程在 500 毫秒后执行 RequestStop()
方法修改了 _shouldStop
的值,但工作线程始终都获取不到 _shouldStop
最新的值,也就永远都不会终止 while
循环。
我们修改一下程序,对 _shouldStop
字段加上 volatile
关键字:
public class Worker
{
private volatile bool _shouldStop;
public void DoWork()
{
bool work = false;
// 获取的是最新的 _shouldStop 值
while (!_shouldStop)
{
work = !work; // do sth.
}
Console.WriteLine("工作线程:正在终止...");
}
// ...(略)
}
此时在主线程调用 RequestStop()
方法后,工作线程便立即终止了,运行效果如下图所示:
这说明加了 volatile
关键字后,程序可以实时读取到字段的最新值。
注意,一定要切换为 Release 模式运行才能看到 volatile
发挥的作用,Debug 模式下即使添加了 volatile
关键字,编译器也是不会执行优化的。