0. 参考资料
刘铁猛C#教程(搬运):Lua && C# (bilibili.com) (我还需要多看几遍:P19~P22、P30)
b站up主小Joe的教程:BeaverJoe的个人空间_哔哩哔哩_bilibili
1. 委托、事件
<1> 总结
要慎用委托与事件,了解委托与接口的利弊(一般处理事件时使用委托,其它情况下则使用接口),事件常用于处理UI控件的响应(客户端开发)。
<2> 委托
委托是一种类。
委托的缺点:
- ① 这是一种方法级别的耦合,现实工作中要慎之又慎。
- ② 使可读性下降,debug的难度增加。
- ③ 把委托回调、异步调用、多线程纠缠在一起,会让代码变得难以阅读与维护。
- ④ 委托使用不当有可能造成内存泄露和程序性能下降。
<3> 事件
事件比委托更安全易懂,事件的本质是委托字段的一个包装器,这个包装器对委托字段的访问起限制作用,事件对外界隐藏了委托实例的大部分功能,仅暴露添加事件、移除事件处理器的功能。
MVC|MVP|MVVM等模式,是事件模式更高级、更有效的实现。
2. C#的数据类型
<1> 数据类型分类
值类型(struct、整型、浮点型、bool、char、enum、元组)
引用类型(class、interface、delegate、record、dynamic)
另外,C#支持*和&,即有指针类型,但这种代码是unsafe的。
<2> 值类型与引用类型的区分
值类型储存在栈上,引用类型储存在堆上。
将值类型转换为引用类型就是装箱,将引用类型转换成值类型就是拆箱。
<3> class类型
数组和字符串都继承自最终基类Object,都是class类型即引用类型。
<4> struct
结构类型 - C# 参考 | Microsoft Learn
C#有垃圾回收机制,即垃圾收集器GC,产生的GC越小性能就越好。
值得注意的是结构体被规定为值类型。即使某个结构体类包含引用类型变量,该结构体依然是值类型。但奇怪的是该结构体会产生更大的GC。(这句话如果有错误,欢迎交流指正)
struct InfoCard
{
int id;
string name;
}
int id;
string name;
C++中的struct几乎可以平替class,但C#里的struct并非如此(缺少继承等class的特性)。由于我个人编码习惯的原因,我在写C++代码时若只需要封装变量我会选择struct,若还需要封装方法时我会选择class,而为Unity写C#代码时我会避免使用struct。
<5> 可空类型
可空类型可以分为可空值类型和可空引用类型。可空类型及其相关操作符(?、??、??=)的可读性我感觉不是很好。
3. 类型转换
强制转换和类型转换 - C# 编程指南 | Microsoft Learn
<1> 自定义类型转换
// 显式类型转换(强制转换)
public static explicit operator A(B b)
{
A a = new A();
// 转换时的属性关系
return a;
}
// 隐式类型转换
public static implicit operator A(B b)
{
A a = new A();
// 转换时的属性关系
return a;
}
若转换会造成精度缺失,则应该定义显示转换,否则定义隐式转换。
<2> 默认转换
我认为使用强制转换需要非常慎重。
// 隐式转换(子类对象->父类对象)
Derived d = new Derived();
Base b = d;
// 强制转换(父类对象->子类对象)
Base b = new Base();
Derived d = b as Derived;
4. 属性与字段
属性(语法糖)是由字段进化而来,其安全性更高。
5. VS代码提示
- 构造函数:输入ctor,按Tab键
- 属性:输入propfull/prop,按Tab键
- 索引器:输入indexer,按Tab键
6. 泛型
类似于C++的模板,可以实现多态,减少代码量。
7. lambda表达式
内联匿名函数。
C# 中的 Lambda 表达式 | Microsoft Learn
C++ 中的 Lambda 表达式 | Microsoft Learn
8. 扩展方法
扩展方法 - C# 编程指南 | Microsoft Learn
static class TransformExtension
{
public static void ResetLocal(this Transform transform)
{
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
}
}
扩展方法真爽,谁用谁知道。
9. C++中STL常用容器对应到C#
C# | C++ |
---|---|
List | std::vector |
LinkedList | std::list |
Dictionary | std::map |
HashSet | std::set |
C#的内置数组应该对应的是C++中的Array还是C++的内置数组呢?如果你知道的话,欢迎在评论区留言。
10. 关键字
(1) in && out
C++中可以通过传const &类型变量提高传参数速度并节省空间,同时又约束该参数不可修改。C#中的in类型与之对应,即in类型就是不可修引用对象的值的ref类型。
简单的示例代码如下。
// C++
#include<iostream>
class Enemy
{
std::string name;
public:
Enemy(const std::string& name) :name(name) {}
const std::string& GetName() { return name; }
};
int main()
{
Enemy meleeAI("A.I - Melee");
Enemy archerAI("A.I - Archer");
std::cout << meleeAI.GetName() << '\n';
std::cout << archerAI.GetName() << '\n';
}
// C#
public class Enemy
{
string name;
public Enemy(in string name) { this.name = name; }
public void GetName(out string name) { name = this.name; }
}
class OutPut
{
public static void Main()
{
Enemy meleeAI = new Enemy("A.I - Melee");
Enemy archerAI = new Enemy("A.I - Archer");
string enemyName;
meleeAI.GetName(out enemyName);
Console.WriteLine(enemyName);
archerAI.GetName(out enemyName);
Console.WriteLine(enemyName);
Console.Read();
}
}
不过C#有字段特别方便。
// C#
public class Enemy
{
public string Name { private set; get; }
public Enemy(in string name) { Name = name; }
}
class OutPut
{
public static void Main()
{
Enemy meleeAI = new Enemy("A.I - Melee");
Enemy archerAI = new Enemy("A.I - Archer");
Console.WriteLine(meleeAI.Name);
Console.WriteLine(archerAI.Name);
Console.Read();
}
}
再来个稍微复杂点的例子。
C#里面操作符的函数属于类(static类型),而C++里面操作符的函数属于类的实例(非static类型)。
另外不难发现上述代码中Vector3是class类型, 创建的变量都是引用类型变量。将Vector3的类型改为struct类型(去掉无参构造),即可让Vector3变为值类型。
C#里面性能并不是那么讲究,至少对于深拷贝与浅拷贝的选择不如C++那么讲究。
// C++
#include <iostream>
class Vector3
{
public:
float x, y, z;
Vector3() :x(0), y(0), z(0) {}
Vector3(float x, float y, float z) :x(x), y(y), z(z) {}
const Vector3 operator-(const Vector3& startPosition) const
{
return Vector3(x - startPosition.x, y - startPosition.y,
z - startPosition.z);
}
void Print() const
{
std::cout << "(" << x << "," << y << "," << z << ")\n";
}
};
class Enemy
{
Vector3 position;
float speed;
public:
Enemy() :position(Vector3()), speed(1) {}
Enemy(Vector3 position, float speed) :position(position), speed(speed) {}
void MoveUp()
{
position.z += speed;
}
const Vector3& GetPosition() const
{
return position;
}
};
int main()
{
Enemy enemy({ 3,3,3 }, 8);
const Vector3& enemyPosition = enemy.GetPosition();
Vector3 oldPosition = enemyPosition;
enemyPosition.Print();
enemy.MoveUp();
enemyPosition.Print();
(enemyPosition - oldPosition).Print();
}
using System;
namespace QCSharp
{
public struct Vector3
{
float x, y, z;
public Vector3(float x, float y, float z)
{
this.x = x;
this.y = y;
this.z = z;
}
public static Vector3 operator -(Vector3 positionA, Vector3 positionB)
{
return new Vector3(positionA.x - positionB.x, positionA.y - positionB.y, positionA.z - positionB.z);
}
public static Vector3 operator +(Vector3 positionA, Vector3 positionB)
{
return new Vector3(positionA.x + positionB.x, positionA.y + positionB.y, positionA.z + positionB.z);
}
public void Print()
{
Console.WriteLine($"({x},{y},{z})");
}
}
public class Enemy
{
public float Speed { private set; get; }
public Vector3 Position { private set; get; }
public Enemy()
{
Position = new Vector3(0, 0, 0);
Speed = 1;
}
public Enemy(in Vector3 position, float speed)
{
Position = position;
Speed = speed;
}
public void MoveUp()
{
Position = new Vector3(0,0,Speed) + Position;
}
}
class OutPut
{
public static void Main()
{
Enemy enemy = new Enemy(new Vector3(3, 3, 3), 8);
Vector3 enemyOldPosition, enemyNewPosition;
enemyOldPosition = enemy.Position;
enemyOldPosition.Print();
enemy.MoveUp();
enemyNewPosition = enemy.Position;
enemyNewPosition.Print();
(enemyNewPosition - enemyOldPosition).Print();
Console.Read();
}
}
}
C#中的out变量是类似于C++中传&类型进去,C#的ref可以完全取代out,那out有什么存在的意义吗?①退出函数前必须为out变量赋值,所以out实际上就是一种约束,提醒开发者这就是输出的变量。②对将要作为out参数的变量赋值无效,而引用必须为之赋值。
主要还是因为②,这样开发者就不用记各种类型的初始值或各种类型就不需要包含初始值属性了。所以实参原先的值无关紧要时都应该用out而不是ref。
// out写法
public static void SplitNumber(int n, out int a, out int b)
{
a = n / 3;
b = n % 3;
}
static void Main()
{
int n = 10;
int a, b;
SplitNumber(n, out a, out b);
Console.WriteLine($"a:{a}, b:{b}");
Console.Read();
}
// ref写法
public static void SplitNumber(int n, ref int a, ref int b)
{
a = n / 3;
b = n % 3;
}
static void Main()
{
int n = 10;
int a = 0, b = 0;
SplitNumber(n, ref a, ref b);
Console.WriteLine($"a:{a}, b:{b}");
Console.Read();
}
(2) partial
partial类可以减少类的派生。
11.
(1) class
a、abstract
修饰函数,约束该类的子类必须实现该函数或将其声明为抽象函数留给下一级子类实现。抽象函数仅声明,无实现。
类中想声明抽象方法,那这个类就必须声明为抽象类,抽象类无法创建实例。
b、Interface
使纯抽象类更加简洁,接口中的函数都是public的。
abstract class Operations
{
public abstract int Add(int a, int b);
public abstract int Sub(int a, int b);
public abstract int Mul(int a, int b);
public abstract int Div(int a, int b);
}
// 接口就是让纯抽象类更加简洁
interface IOperations
{
int Add(int a, int b);
int Sub(int a, int b);
int Mul(int a, int b);
int Div(int a, int b);
}