CSharp
1. 什么是面向对象,和面向过程的区别
面向对象:当解决一个问题时,把事物抽象成对象的概念,看这个问题中有哪些对象,给这些对象赋一些属性和方法,让每个对象去执行自己的方法,最终解决问题
面向过程:当解决一个问题时,把事件拆封成一个个函数和数据,然后按照一定的顺序,依次执行完这些方法(过程),等方法执行完毕,事情也解决。
面向过程性能比面向对象要高,但是没有面向对象易于维护、易于复用、易于扩展
2. 五大基本原则
- 单一职责原则:一个类的功能要单一,不能太复杂。就像一个人一样,分配的工作不能多,不然虽然很忙碌,但是效率并不高
- 开放封闭原则:一个模块在扩展性方面应该是开放的,而在更改性方面应该是封闭的。例如一个模块,原先只有服务端功能,但是现在要加入客户端功能,那么在设计之初,就应当把服务端和客户端分开,公用的部分抽象出来,从而在不修改服务端的代码前提下,添加客户端代码。
- 里式替换原则:子类应当可以替换父类并出现在父类能出现的任何地方。例如参加年会,公司所有的员工应该都要可以参加抽奖,而不是只让一部分员工是参加抽奖。
- 依赖倒置原则:具体依赖抽象,上层依赖下层。例如B是较A低的模块,但B需要使用A的功能,这时B不能直接使用A中的具体类,而是应当由B来定义一个抽象接口,由A来实现这个抽象接口,B只使用这个抽象接口,从而达到依赖倒置,B也接触了对A的依赖,反过来是A依赖于B定义的抽象接口。
- 接口分离原则:模块之间要通过抽象接口隔离开,而不是通过具体的类强耦合起来
3. 面向对象的特征?如何设计和实现面向对象?
- 封装:将数据和行为相结合,通过行为来进行约束代码,从而增加数据的安全性
- 继承:用来扩展一个类,子类可以继承父类的部分行为和属性,从而便于管理、提高代码的复用
- 多态:一个对象,在不同情形有不同表现形式;子类对象可以赋值给父类型的变量
将不同的功能模块封装成不同的类,通过继承和多态等特性来实现代码的复用和扩展
4. 重载、重写
- 两者定义的方式不同 ;重载方法名相同参数列表不同,重写方法名和参数列表都相同
- 封装、继承、多态所处的位置不同;重载是在同类中,而重写是在父子类中
- 多态的时机不同,重载是在编译时多态,而重写是在运行时多态
5. 不安全代码
当一个代码使用unsafe修饰符标记时,c#允许在函数中使用指针变量。
6. 值类型、引用类型
- 值类型存储在内存栈中,超过作用域自动清理,在作用域结束后会被清理,而引用类型存储在内存堆中,由GC来自动释放,
- 值类型转换成引用类型(装箱)要经历分配内存和拷贝数据
- 引用类型转换成值类型(拆箱)要首先找到属于值类型的地址,再把引用对象的值拷贝到内存栈中的值实例中
- 值类型存取速度快,引用类型存取速度慢
- 值类型表示实际的数据,引用类型表示存储在内存堆中的数据的指针和引用
- 值类型的基类是ValueType,引用类型的基类是Object
- 值类型在内存管理方面有更好的效率,且不支持多态,适合用作存储数据的载体
引用类型支持多态,适合用于定义应用程序的行为
- 值类型有:byte、short、int、long、double、decimal、char、bool、struct
引用类型有:array、class、interface、delegate、Object、string
7. 接口、抽象类
接口:
- 契约式设计,是开发的,谁都可以去实现,但是必须要遵守接口的约定;例如所有游戏都必须会有主角给玩家进行操作,这种必须规范
- 是约束类应该具有的功能集合,从而便于类的扩展和管理
- 接口不能实例化
- 接口关注于行为,可以多继承
- 接口的设计目的,是对类的行为进行约束,接口隔离(提供一种机制,可以强制要求不同的类具有相同的行为,只约束了行为的有无,但是不对如何去实现行为进行约束)
- 类可以实现一个或多个接口,从而拥有接口中定义的所有方法。
public interface IShape
{
double Area(); // 接口方法,类必须实现
}
public class Circle : IShape
{
private double radius;
public Circle(double r)
{
radius = r;
}
public double Area() // 实现接口方法
{
return Math.PI * radius * radius;
}
}
public class Rectangle : IShape
{
private double width;
private double height;
public Rectangle(double w, double h)
{
width = w;
height = h;
}
public double Area() // 实现接口方法
{
return width * height;
}
}
class Program
{
static void Main(string[] args)
{
Circle circle = new Circle(5);
Console.WriteLine("Circle area: " + circle.Area());
Rectangle rectangle = new Rectangle(4, 6);
Console.WriteLine("Rectangle area: " + rectangle.Area());
}
}
抽象类:
- 对同类事物相对具体的抽象,例如我会玩游戏,你如果继承了我,你也必须会玩游戏,但是我不管你玩什么类型游戏;
- 不能实例化,它只能被用作其他类的基类
- 是概念的抽象,只能单继承
- 可以包含抽象方法和具体方法,子类必须实现抽象方法
- 用于实现共享行为和属性,以及定义子类必须实现的方法
- 设计目的,是代码复用(当不同的类具有某些相同的行为,且其中一部分行为的实现方法一致时,可以让这些类都派生于一个抽象类。在这个抽象类中实现了公用的行为,从而可以避免让所有的子类来实现该行为,从而让子类可以自己去实现抽象类中没有的部分)
- 一个类可以有多个接口,但是只能继承一个父类
public abstract class Shape
{
public abstract double Area(); // 抽象方法,子类必须实现
public virtual void Draw() // 虚方法,子类可以选择性地重写
{
Console.WriteLine("Drawing shape");
}
}
public class Circle : Shape
{
private double radius;
public Circle(double r)
{
radius = r;
}
public override double Area() // 实现抽象方法
{
return Math.PI * radius * radius;
}
public override void Draw() // 重写虚方法
{
Console.WriteLine("Drawing circle");
}
}
public class Rectangle : Shape
{
private double width;
private double height;
public Rectangle(double w, double h)
{
width = w;
height = h;
}
public override double Area() // 实现抽象方法
{
return width * height;
}
}
class Program
{
static void Main(string[] args)
{
Circle circle = new Circle(5);
Console.WriteLine("Circle area: " + circle.Area());
circle.Draw();
Rectangle rectangle = new Rectangle(4, 6);
Console.WriteLine("Rectangle area: " + rectangle.Area());
rectangle.Draw(); // 使用父类的虚方法实现
}
}
8. 字段、属性、索引器
字段: 是一种表示与对象或类相关联的变量的成员,字段的声明用于引入一个或多个给定类型的字段。
属性: 是对字段的封装,将字段和访问字段的方法组合在一起,提供灵活的机制来读取、编写或计算私有字段的值。并且属性通过Get访问器和set访问器来控制对字段的访问;
索引器:类似数组的访问方式,通过索引来访问对象中的特定元素,而不必暴露对象的内部结构。
例如:
pulic class User
{
private string _name;//_name为字段
public string Name //Name为属性,它含有代码块
{
get
{
return _name;//读取(返回_name值)
}
set
{
_name = value;//为_name赋值
}
}
如上,属性以灵活的方式实现了对私有字段的访问,是字段的自然扩展,一个属性总是与某个字段相关联,字段能干的,属性一定能干,属性能干的,字段不一定干的了;为了实现对字段的封装,保证字段的安全性,产生了属性,其本质是方法,暴露在外,可以对私有字段进行读写,以此提供对类中字段的保护,字段中存储数据更安全。
——参考原文链接:字段和属性
9. 什么是lambda表达式
一个用来代替委托实例的匿名方法(本质是一个方法,只是没有名字,任何使用委托变量的地方都可以使用匿名方法赋值),从而让代码更加简洁。被Lambda表达式引用的外部变量叫做被捕获的变量;捕获了外部变量的Lambda表达式叫做闭包;被捕获的变量是在委托被实际调用的时候才被计算,而不是在捕获的时候
//不使用Lambda表达式时的一个点击事件
public class button : MonoBehaviour
{
public Button btn;
void Start ()
{
btn.onClick.AddListener(btnClick);
}
void btnClick()
{
Debug.Log("按钮被点击了!");
}
}
-------------------------------------------------------------------
-------------------------------------------------------------------
//使用Lambda表达式时的一个点击事件
public class button : MonoBehaviour
{
public Button btn;
void Start ()
{
//无参匿名函数
btn.onClick.AddListener(()=>
{
Debug.Log("按钮被点击了");
}
}
}
public class LambdaTest : MonoBehaviour
{
public delegate int MyDelegate(int t);
[ContextMenu("Exe")]
private void Start()
{
/** 实际上,编译器会通过编写一个私有方法来解析这个lambda表达式
* 然后把表达式的代码移动到这个方法里
**/
MyDelegate my = (x) => { return x * x; };//在只有一个可推测类型的参数时,可以省略小括号
MyDelegate my1 = (x) => x * x;
int num = my(2);
int num1=my1(2);
Debug.Log(num); //4
Debug.Log(num1); //4
}
}
10. Func和Action
Func:是一个泛型委托类型,它表示一个具有指定返回类型的方法。Func 委托可以接受 0 到 16 个输入参数,并返回一个指定类型的结果。例如,Func<int, string> 表示一个接受一个整数参数并返回一个字符串结果的方法。
Func<int, int, int> add = (x, y) => x + y; //前两个为传的参数,最后一个为返回值
int result = add(3, 5); // 结果为 8
12
Action: 也是一个委托类型,但它表示一个没有返回值的方法。与 Func 不同,Action 委托可以接受 0 到 16 个输入参数,但不返回任何值。
Action<string> logMessage = message => Console.WriteLine(message);
logMessage("Hello, world!"); // 打印 "Hello, world!"
12
11. foreach迭代器循环遍历和for循环遍历的区别
For循环通过下标,对循环中的代码反复执行,功能强大,可以通过Index(索引)去取得元素。
Foreach循环从头到尾,对集合中的对象进行遍历。在遍历的时候,会锁定集合的对象,期间是只读的,不能对其中的元素进行修改。
Foreach相对于For循环,其运行时的效率要低于For循环,内存开销也比For循环要高些。但是,在处理一些不确定循环次数的循环或者循环次数需要计算的情况时,使用Foreach更方便些。
12. is和as
两者都是用于类型检查和类型转换用的关键字
is:用于检查一个对象是否是某个特定类型的实例。它返回一个bool值,如果对象是指定类型的实例,则返回true,否则为false
object obj = "Hello";
if (obj is string)
{
Console.WriteLine("obj is a string");
}
as:用于将一个对象强制转换为指定类型,如果转换成功则返回转换后的对象,如果失败则返回null
object obj = "Hello";
string str = obj as string;
if (str != null)
{
Console.WriteLine("Successfully cast to string: " + str);
}
13. Async和Await的用法
两者都是用于异步编程的关键字
Async:用于声明一个异步方法,异步方法可以包含“await” 表达式,该关键字告诉编译器在等待异步操作完成时暂停当前玩法的执行(返回Task对象,对象可带结果可不带)
async Task<string> DownloadDataAsync()
{
HttpClient client = new HttpClient();
string result = await client.GetStringAsync("https://example.com");
return result;
}
Await:用于等待异步操作完成,并获取其结果。在异步方法中,"await"会将执行控制权返回给调用方,直到异步操作完成为止。通常情况下,"await"表达式会应用于返回"Task"或"Task< TResult >"对象的异步方法调用上
string data = await DownloadDataAsync();
Console.WriteLine("Downloaded data: " + data);
14. string和stringBuilder和stringBuffer的区别
都是用来处理字符串的类,但是有些重要的区别
string:
- 是不可变的,一旦创建就不能被修改
- 对string进行的修改实际上是创建了一个新的string对象,也就是引用类型
- 因为每次对string的操作都是创建新的对象,这样会产生额外的内存开销
stringBuilder:
- 是可变的,可以动态修改字符串的内容
- 性能比string要好,特别是在需要多次修改字符串时
- 使用“Append”的方法来追加字符串,而不是创建新的字符串
stringBuffer
- 和stringBuilder类似,都是可变的字符串类,只不过stringBuffer是线程安全的,而stringBuilder不是。因此在多线程情况下,推荐使用stringBuffer,而在单线程情况下,使用stringBuilder会更高效,当然,前提是有较多的动态修改字符串内容的需求下,否则还是建议使用string
15. 什么是GC垃圾管理器?产生的原因?如何避免?
GC垃圾回收是一种自动内存管理机制,用于在程序运行时自动检测和释放不再使用的内存对象,以避免内存泄漏和减少内存碎片。
其产生的原因主要是为了解决手动管理内存的问题,例如忘记释放内存或释放了仍在使用的内存,从而导致内存泄漏和错误
如何避免可以遵循以下原则
- 合理使用对象生命周期:尽量在不需要使用对象时手动释放对象引用,避免对象长时间存活于内存中
- 避免创建过多临时对象:在循环或频繁调用的方法中,应尽量避免创建大量临时对象,可以通过对象池等技术来复用对象,减少GC压力
- 使用合适的数据结构和算法
- 减少频繁的创建或销毁对象操作,尽量复用已有对象
16. 反射的实现原理
是指在程序运行时动态的获取类的信息以及操作类的成员和属性的机制。它允许程序在运行时检查和修改对象的结构、行为和属性,而不是在编译时确定这些信息。但是这样会带来一定的性能开销,通常比静态代码更慢,因为涉及到动态解析和调用,因此在性能需求较高的情况下,应该尽量避免过度使用反射。
原理主要涉及以下方面
- 元数据:在.NET平台下,每个程序集都包含元数据,其中包含了有关程序集、类型、成员和其他信息的描述。这些元数据可以在运行时被反射机制所访问和利用
- 类型加载和装载:当程序需要使用某个类型时,CLR(通用语言执行平台(Common Language Runtime,简称CLR)是微软为他们的.NET的虚拟机所选用的名称。它是微软对通用语言架构(CLI)的实现版本,它定义了一个代码执行的环境。CLR执行一种称为通用中间语言的字节码,这个是微软的通用中间语言实现版本)会负责加载和装载该类型的元数据信息。这些元数据包含了类型的结果、成员和方法等信息。
- Type类和MemberInfo类:在.Net中,Type类和MemberInfo类是反射的核心,它们提供了许多方法和属性,可以用于动态的获取和操作类型信息和成员信息。通过这些类,可以获取类的名称、属性、方法、字段等信息,也可以调用对象的方法和属性
- 动态调用:反射允许程序在运行时动态的调用方法、属性和构造函数,而不需要在编译时确定调用哪个方法或属性
17. 虚方法
- 在基类中声明的可重写的方法,子类可以选择性地重写虚方法
- 如果在派生类中未重写虚方法,则调用基类中的方法
- 用于实现方法的重写和多态性
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Dog barks");
}
}
18. 什么是泛型?好处是什么?
是一种编程机制,允许在编写代码时使用参数化类型,从而实现代码的灵活性和重用性。在定义类、结构、接口和方法时使用类型参数,这些类型参数在使用时可以被替换为具体的数据类型,从而使得代码可以处理各种不同类型的数据,而无需针对每种数据类型编写单独的代码。
- 代码复用:可以让代码变得更加通用,可以处理多种不同类型的数据
- 类型安全:泛型在编译时会进行类型检查,可以提前发现类型错误,从而避免在运行时出现类型不匹配
- 性能优化:泛型在编译时会生成特定类型的代码,避免了装箱拆箱,从而提高了执行效率
- 抽象数据结构:泛型使得可以定义抽象的数据结构,例如列表、栈、队列等,可以适用各种不同类型的数据,提高了数据结构的通用性和灵活性
- 更好的API设计:可以提供更加抽象和通用的接口,满足不同场景的需求
20. Ref和Out关键字有什么区别?Ref的深层原理是什么?
两者都用于参数传递
ref:
- 用于将参数按引用传递给方法,这意味着对参数的任何更改都会影响到原始变量;
- 在调用方法前,必须初始化ref参数
out:
- 也用于按引用传递参数,但通常用于从方法返回多个值
- 在调用方法前,不需要初始化out参数,因为它们被视为为赋值的输出参数,但必须在退出之前为out参数赋值
ref的深层原理涉及到C#中的参数传递机制->即按值传递和引用传递
- 值传递:对于值类型,如int、double等,传递的是参数的副本,方法内对参数的修改并不会影响到原始变量
- 引用传递:对于引用类型参数,如类、数组等,传递的是地址,方法内对参数的修改会影响到原始变量,而ref关键字实际上是一种引用传递
21. Using的作用
- 声明命名空间:以便于在代码中使用该命名空间中的类型而无需使用完全限定名
using System;
// 在代码中可以直接使用 System 中的类型,而无需完全限定名
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, world!");
}
}
- 声明资源的使用范围:通常用于确保在代码块结束时释放资源,即使发生异常也能够安全的释放资源。这种用法通常用于处理需要显示释放的资源,如文件、数据库连接等
using (var stream = File.Open("example.txt", FileMode.Open))
{
// 使用文件流进行读取或写入操作
} // 在此处,文件流会被自动释放
22. 引用和指针的区别
引用:
- 是一种对对象的间接访问方式,它提供了一种安全且易于使用的方式来访问内存中的对象
- 在C#中,大多数情况下,我们使用引用来操作对象,而不是直接操作内存地址。
- 引用不直接暴露内存地址,而是提供了一种更高级别的抽象,使得对象的创建、销毁和操作更加简单和安全
- 引用在编译时就已经确定了对象的类型,因此是类型安全的
指针:
- 是一种直接访问内存地址的方式,在C#中,使用指针通常需要使用不安全代码块,并且要谨慎的处理以避免内存错误
- 指针提供了对内存中特定位置的直接访问能力,允许我们绕过编译时类型检查和安全检查
- 使用指针可以在一些特殊情况下提供更高效的内存操作,但是也增加了代码的复杂性和风险
综上,引用是一种更安全和更高级别的对象访问方式,而指针则是一种更底层、更危险的内存访问方式,需要谨慎使用。因此在C#中,通常情况下都去使用引用来操作对象,而避免直接使用指针
23. 结构体和类的区别
- 类是引用类型,使用‘class’关键字定义
- 结构体是值类型,使用‘struct’关键字定义
- 类的实例存储在堆内存中,而在栈内存中存储的是对堆中实例的引用
- 结构体的实例通常存储在栈内存中,除非作为字段嵌入在类中或者显式地使用‘new’进行堆分配
- 类的实例分配和释放通常需要在堆上进行内存分配和垃圾回收,因此在性能和内存开销上可能更高
- 结构体的实例通常被分配在栈上,因此在内存分配和访问上可能更加高效
- 类支持继承和多态,可以通过继承现有类来扩展和修改行为
- 结构体不支持继承,因为是密封的,并且无法作为基类来派生新的类型
- 类的实例在赋值或传递给方法时,实际上是传递引用,多个变量指向同一个对象
- 结构体的实例在赋值或传递给方法时,会进行值拷贝,每个变量都有自己的独立副本
- 类可以有默认构造函数,也可以自定义构造函数
- 结构体默认具有无参构造函数,但是也可以定义自定义构造函数
综上,当需要表示较复杂的数据结构、支持继承和多态、以及希望共享对象实例时,通常会选择类。而当需要表示简单的数据类型、避免堆分配、或者需要频繁的值拷贝时,可以考虑使用结构体
24. 栈和堆的区别
- 栈是一种先进后出的数据结构,数据项也按照先进后出的顺序存储和访问
- 堆是一种树状数据结构,主要用来动态分配内存,存储的数据项没有固定的顺序,可以根据需要动态分配和释放
- 栈的分配是静态的,由编译器或运行时系统进行管理,通常在程序运行时就已经确定了变量的生命周期和内存需求
- 堆的分配是动态的,由程序员在运行时根据需要显性地请求内存分配,并负责在不再需要时释放内存
- 栈的访问速度比堆更快,因为栈上的数据项是按照固定的顺序存储,可以直接通过指针进行访问。
- 堆的访问速度相对较慢,因为堆上的数据项存储在不同的位置,需要通过指针来定位和访问
- 栈上的数据项的生命周期是由其所在的作用域决定的,当作用域结束时,栈上的数据会自动销毁
- 堆上的数据项的生命周期由程序员管理,需要手动的分配和释放内存,否则可能导致内存泄漏或者内存溢出
- 栈的内存管理的自动的,由编译器或运行时系统进行管理
- 堆的内存管理的手动的,需要程序员手动的分配和释放
综上,栈适合存储局部变量和函数调用等临时数据,而堆更适合存储动态分配的对象和数据结构
25.什么是解耦、内聚
解耦是将系统中的各个组件或模块之间的耦合度降到最小程度,使其相互之间的依赖关系减弱或消除。就像积木一样,各个积木可以组合在一起从而形成一个形状,又可以拆分,又可以替换,因为在理想状态下,每个模块就像积木一样,都是独立的个体,只要他们之间的接口相匹配,就可以灵活的搭配在一起,而解耦,就是朝这个理想状态而做的事情
解耦通常可以以下操作来实现:
- 接口抽象:定义接口来描述模块之间的通信和交互方式,而不是直接依赖于具体的实现类
- 依赖注入:通过依赖注入的方式将组件之前的依赖关系交给外部管理,使得组件之间解耦合
- 事件驱动:使用事件机制来实现模块之前的通信和解耦,一个模块产生事件,而其他模块监听并响应这些事件
- 消息队列:通过消息队列来实现模块之间的异步通信,降低模块之间的耦合度
- 中介者模式:引入中介者来管理模块之间的通信,将复杂的交互逻辑集中到中介者中,从而减少模块之前的直接依赖关系
内聚是指一个模块或类中的各个成员之间相互联系的紧密程度。高内聚表示模块内部的各个成员彼此之间紧密联系,共同完成一个特定的功能或目标,而低内聚则表示模块内部的各个成员之间联系较少功能分散,没有明确的目标
内聚度通常分为以下类型
- 功能内聚:模块内的各个成员都在完成同一个功能或任务。这种内聚度是最高的,模块的功能非常集中和明确,易于理解和维护。
- 顺序内聚:模块内的各个成员按照执行顺序相关联,一个成员的输出作为下一个成员的输入。虽然各个成员之间有联系,但是功能没有集中,模块的职责较为模糊
- 通信内聚:模块中的各个成员之间通过传递数据进行通信,彼此之间交流频繁。这种内聚度较高,但是模块的功能可能较为松散。
- 过程内聚:模块内的各个成员都在完成某个过程或算法,但彼此之间关联不强。这种内聚度较低,模块的职责分散。
- 时间内聚:模块内的各个成员在某个时间段内一起执行,但彼此之间关联不强。这种内聚度也较低,模块的功能和职责不够集中
综上,高内聚低耦合,是良好的设计原则。这样可以提高模块的可理解性、可维护性和可扩展性,同时降低代码的复杂度。
26.委托和事件
委托是一个类,它定义了方法的类型,使得可以将多个方法赋给同一个委托变量,当调用这个变量时,将依次调用其绑定的方法
一般调用方法,我们称为直接调用方法,而委托可以间接调用方法,也就是委托封装了一个或多个方法,方法的类型必须与委托的类型兼容;同时委托也可以当做方法的参数,传递到某个方法中去,当使用这个传进来的委托时,就会间接的去调用委托封装的方法,从而形成动态的调用方法的代码,并且降低了代码的耦合性。
可以理解为是一种类型安全的函数指针,用于引用具有相同签名的方法。
using UnityEngine;
//声明委托,可以在类的外面,也可以在类的内部
public delegate X ADelegate<in T, out X>(T t);//有返回值的委托 对应Func委托 in为输入 out输出
public delegate void BDelegate<in T>(T t);//无返回值的委托 对应Action委托
public class DelegateTest : MonoBehaviour
{
public ADelegate<int, string> MyTest;
[ContextMenu("Exe")]
private void Start()
{
MyTest = IntNumber;
TestDelegate(MyTest);//输出 返回值:100
//目标实例(当目方法为静态时,目标实例为null)
Debug.Log(MyTest.Target); //输出 DelegateTest
//实例方法
Debug.Log(MyTest.Method); //输出 System.String IntNumber(Int32)
//(返回值类型)(目标方法)(方法参数)
}
public string IntNumber(int a) => (a * a).ToString();
public void TestDelegate(ADelegate<int, string> aDelegate) => Debug.LogFormat("返回值:{0}", aDelegate?.Invoke(10));
}
using System;
using UnityEngine;
public class DelegateTest : MonoBehaviour
{
[ContextMenu("Exe")]
private void Start()
{
TestDelegate(Add);
TestDelegate(Sub);
//-----------委托与接口类似,委托能实现的,接口也能实现,但又有所不同
TestInterface(new Add());
TestInterface(new Sub());
/*
* 对于这个例子这种情况,更适合使用委托,因为接口而言,要多次实现接口,过于麻烦。
*/
}
public int Add(int num) => num + num;
public int Sub(int num) => num - num;
public void TestDelegate(Func<int ,int> _func) => Debug.LogFormat("返回值:{0}", _func?.Invoke(10));
public void TestInterface(ICalcNumber calc) => Debug.LogFormat("返回值:{0}", calc.Calc(10));
}
public interface ICalcNumber
{
public int Calc(int num);
}
class Add : ICalcNumber
{
public int Calc(int num) => num + num;
}
class Sub : ICalcNumber
{
public int Calc(int num) => num - num;
}
事件,他可以让一个类或对象去通知其他类、对象,并做出相应的反应;是封装了委托类型的变量,使得在类的内部,不管是public和protected,总是private的。在类的外部,注册"+=“和注销”-="的访问限制符和在声明事件时使用的访问符一致。其主要目的就是为了防止订阅者之间相互干扰。
是委托的一种特殊用法,用来通知其他对象发生了特定的事件
using System;
using UnityEngine;
/// <summary>
///
/// * Writer:June
/// *
/// * Data:2021.5.12
/// *
/// * Function:事件例子=====>顾客点单
/// *
/// * Remarks:
///
///
/// 事件模型的五个组成部分
/// 事件的拥有者: 类
/// 事件: event关键字修饰
/// 事件的响应者: 类
/// 事件处理器: 方法-受到约束的方法
/// 事件的订阅关系: +=
///
/// </summary>
/* 用于事件约束的委托类型,签名应该遵循:事件名+EventHandler
* (当别人看到这个后缀之后,就知道,这个委托是用来约束事件处理器的,不会用于做别的事!!!)
*/
public delegate void OrderEventHandler(Customer _customer, OrderEventArgs _oe);
public class EventEx : MonoBehaviour
{
//顾客类实例化
Customer customer = new Customer();
//服务员类实例化
Waiter waiter = new Waiter();
private void Start()
{
customer.OnOrder += waiter.TakeAction;
customer.Order();
customer.PayTheBill();
}
}
public class Customer
{
public float Bill { get; set; }
public void PayTheBill() => Debug.Log("我应该支付:" + Bill);
#region 事件的完整声明格式
//private OrderEventHandler orderEventHandler;
//public event OrderEventHandler OnOrder
//{
// add { orderEventHandler += value; }//事件添加器
// remove { orderEventHandler -= value; }//事件移除器
//}
//public void Order()
//{
// if (orderEventHandler != null)
// {
// OrderEventArgs orderEventArgs = new OrderEventArgs
// {
// CoffeeName = "摩卡",
// CoffeeSize = "小",
// CoffeePrice = 28
// };
// orderEventHandler(this, orderEventArgs);
// OrderEventArgs orderEventArgs1 = new OrderEventArgs
// {
// CoffeeName = "摩卡",
// CoffeeSize = "小",
// CoffeePrice = 28
// };
// orderEventHandler(this, orderEventArgs1);
// }
//}
#endregion
#region 事件的简略声明格式
public event OrderEventHandler OnOrder;
public void Order()
{
//语法糖衣
if (OnOrder != null)
{
OrderEventArgs orderEventArgs = new OrderEventArgs
{
CoffeeName = "摩卡",
CoffeeSize = "小",
CoffeePrice = 28
};
OnOrder(this, orderEventArgs);
OrderEventArgs orderEventArgs1 = new OrderEventArgs
{
CoffeeName = "摩卡",
CoffeeSize = "小",
CoffeePrice = 28
};
OnOrder(this, orderEventArgs1);
}
}
#endregion
}
public class OrderEventArgs : EventArgs
{
public string CoffeeName { get; set; }
public string CoffeeSize { get; set; }
public float CoffeePrice { get; set; }
}
public class Waiter
{
//事件处理器
internal void TakeAction(Customer _customer, OrderEventArgs _oe)
{
float finaPrice = 0;
switch (_oe.CoffeeSize)
{
case "小":
finaPrice = _oe.CoffeePrice;
break;
case "中":
finaPrice = _oe.CoffeePrice + 3;
break;
case "大":
finaPrice = _oe.CoffeePrice + 6;
break;
default:
break;
}
_customer.Bill += finaPrice;
}
}
简而言之,委托是事件的底层基础,事件是委托的上层建筑,类似于字段和属性之间的关系,属性包装着字段,通过一系列的逻辑来保护字段。事件也是如此,起到保护委托类型,以免被外界滥用。他只能通过+=、-=来添加和移除事件处理器,不能直接的外部进行访问和调用
——参考原文链接:事件和委托详解1
——参考原文链接:事件和委托详解2
27. 异常捕获
允许程序员识别、捕获和处理出现的异常,保证程序在出现异常时仍能够以一种较合理的方式进行处理,而不会导致程序崩溃或不可预测的行为
当程序出现错误或意外情况时,可以使用’throw‘关键字手动抛出一个异常对象(通常是某个异常类的实例,用于描述异常的类型和信息)
在代码中使用‘try-catch’来捕获可能抛出的异常,‘try’块用于包裹可能抛出异常的代码块,而‘catch’块用于处理捕获到的异常
28.如何对大量数据进行高效处理?
- 使用适当的数据结构:例如需要频繁地执行插入、删除和查找操作,可以使用哈希表(dictionary)或平衡树(红黑树)等高效的数据结构
- 将大量数据分割成较小的数据块,然后分别处理每个数据块,最后合并结果
- 选择合适的算法对数据进行处理,以减少时间复杂度和空间复杂度。例如需要对大量数据进行排序,可以使用快速排序或归并排序,而不是简单的冒泡排序和插入排序(详情可见下面的算法)
- 并行化处理,利用多线程或并行计算技术来同时处理多个数据
- 使用数据流(以连续、增量方式大量发送的数据,目标是低延迟处理)处理技术来处理大量数据
29.多线程编程
通常使用Thread类、Task类或线程池来实现。并且在过程中可能会遇到线程安全、死锁、资源竞争等问题,那么为了解决这些问题,可以使用锁、信号量、互斥量等同步机制,以及采用适当的线程安全数据结构来确保多线程的正确性和性能
30.什么是LINQ?
是C#中的语言集成查询功能,可以让开发人员使用类似SQL的语法来进行数据查询和操作
// 指定数据源
int[] scores = [97, 92, 81, 60];
// 定义查询表达式
IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;
// 执行查询
foreach (var i in scoreQuery)
{
Console.Write(i + " ");
}
// 输出: 97 92 81
31.C#修饰符有哪些?有什么区别?
- 访问修饰符:类、属性和方法的定义分级制度;一个成员只能有一个访问修饰符,使用 protected internal组合时除外。
- Public:公有的,访问无限制
- Internal:内部的,仅同一个程序集中可访问
- Private:私有的,只有在声明它的类和结构中可访问
- Protected:受保护的,仅在该类和其派生类中可访问
- abstract: 抽象类,可以被指示一个类只能作为其它类的基类.
- sealed: 表示一个类不能被继承.
- static:修饰类时表示该类是静态类,不能够实例化该类的对象,该类的成员为静态.
- abstract:指示该方法或属性没有实现.
- const:指定域或局部变量的值不能被改动.
- event:声明一个事件.
- extern:指示方法在外部实现.
- override:对由基类继承成员的新实现.
- readonly:指示一个域只能在声明时以及相同类的内部被赋值.
- Partial:在整个同一程序集中定义分部类和结构
- Virtual:用于修饰方法、属性、索引器或事件声明,并且允许在派生类中重写这些对象
- New:作修饰符,隐藏从基类成员继承的成员,在不使用 new 修饰符的情况下隐藏成员是允许的,但会生成警告。作运算符,用于创建对象和调用构造函数。