文章目录
- 引用类型
- class
- 匿名类
- 记录
- 引用相等和值相等
- record声明
- 接口
- delegate 委托
- 合并委托/多路广播委托
引用类型
引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据。 对于引用类型,两种变量可引用同一对象;因此,对一个变量执行的操作会影响另一个变量所引用的对象。 对于值类型,每个变量都具有其自己的数据副本,对一个变量执行的操作不会影响另一个变量(in、ref 和 out 参数变量除外)。
在前文中,我们常常说引用类型比值类型要好,一方面引用类型的对象类似于指针,不同的变量会指向同一个对象。而值类型则是将定义的值克隆一个副本赋值给新的变量。从性能方面引用类型就更好,并且从内存方面考虑,引用类型存放在CLR的虚拟机堆中,而值类型存放在栈中,在堆中可以对数据直接处理,而在栈中则需要将其前面的内存出栈,从内存方面使用引用类型也更好。而具体的关于引用和值类型在内存中的关系,将在未来GC的章节中描述。
class
类应该是最熟悉的一种引用类型了
class ClassA { } --直接定义类
class DerivedClass : BaseClass { } --继承单个类
class ImplClass : IFace1, IFace2 { } -- 实现两个接口
class ImplDerivedClass : BaseClass, IFace1 { } --继承一个类,实现一个接口
在C#中,我们只能继承一个类,但是可以继承多个接口。(看到本文,我默认你学过面向对象了)
匿名类
匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断。
var v = new { Amount = 108, Message = "Hello" };
// Rest the mouse pointer over v.Amount and v.Message in the following
// statement to verify that their inferred types are int and string.
Console.WriteLine(v.Amount + v.Message); //108Hello
使用匿名类,我们定义新的对象的时候可以不必定义一个类,这是为了方便我们进行一些对象的生成,特别是当这个类是临时定义的,只需要属性而不需要其他方法,且有可能只会定义一次对象的时候。
匿名类型包含一个或多个公共只读属性。 包含其他种类的类成员(如方法或事件)为无效。 用来初始化属性的表达式不能为 null、匿名函数或指针类型。
在下例中,我们用prod遍历了products集合,并在每次创建一个新的匿名类,其中属性为Color和Pirce
var productQuery =
from prod in products
select new { prod.Color, prod.Price };
foreach (var v in productQuery)
{
Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
尽管我们并没有给匿名类中的属性创建变量名,但是匿名类自动的将创建时的变量名作为了自己的变量名,也就是创建时使用了prod.Color,prod.Price
。它根据变量名为自己的属性也创建了相应的同名变量Color
和Price
。
还可以按另一种类型(类、结构或另一个匿名类型)的对象定义字段。 它通过使用保存此对象的变量来完成,如以下示例中所示,其中两个匿名类型是使用已实例化的用户定义类型创建的。 在这两种情况下,匿名类型 shipment
和 shipmentWithBonus
中的 product
字段的类型均为 Product
,其中包含每个字段的默认值。 bonus
字段将是编译器创建的匿名类型。
var product = new Product();
var bonus = new { note = "You won!" };
var shipment = new { address = "Nowhere St.", product };
var shipmentWithBonus = new { address = "Somewhere St.", product, bonus };
在上述例子中我们可以看到,匿名类本身的属性通常不需要定义类型,而类型是由编译器自己决定的。而已有类型定义的变量则是其对应的类型,例如product
的类型是Product
,而bonus
的类型则是匿名类。
由于匿名类的类型是不定的,因此使用var
关键字来定义这个匿名类。
如果想要修改匿名类中的属性,可以使用with表达式来实现:
var apple = new { Item = "apples", Price = 1.35 };
var onSale = apple with { Price = 0.79 };
var Banana = apple with {}; // 使用with空值可以直接赋值
(注意,with和record只能在C# 9以上的版本使用,unity中暂时不可用)
记录
记录是一个类或者结构体,通过添加record
修饰符声明,
在下列情况下,请考虑使用记录而不是类或结构:
- 你想要定义依赖值相等性的数据模型。
- 你想要定义对象不可变的类型。
值相等性的意思是两个作为相等比较的值,只有当它们类型相等,且属性的属性名相等且属性值相等,才能视为相等。类似于内部的键值对需要完全相等。更具体的来说,一般而言的相等性是引用相等性,其相等比较在于:如果两个类型引用同一个变量则认为它们相等。
不可变性就是在对象实例化后禁止更改该对象的任何属性或字段值。
引用相等和值相等
为了清楚引用相等和值相等,请看下面几个例子:
Product A = new Product();
Product B = new Product();
if (A == B)
{
Debug.Log(1); --输出不了
}
第一个例子中,A==B是false的,因为A和B作为引用类型,它们的引用不相等
Product A = new Product();
Product B = A;
if (A == B)
{
Debug.Log(1); --输出1
}
在第二个例子中,B引用A,而A引用的是new Product(),所以二者引用相等。
值相等应当不必解释了,就是平常意义上的相等
record声明
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
接口
接口是高度抽象的,我们可以使用interface
关键字定义一个接口,在接口中我们只应当定义函数头,其内部实现则完全由继承了接口的类来实现。
interface IEquatable<T>
{
bool Equals(T obj);
}
一个接口同样可以继承其他的接口,当一个接口继承多个其他接口后,这个接口被称为派生接口,其他接口被称为基接口。一个继承了派生接口的类,不仅需要实现派生接口中的所有方法,也需要实现基接口中的所有方法:
interface IBaseInterface
{
void BaseMethod();
}
interface IEquatable : IBaseInterface
{
bool Equals();
}
public class TestManager : IEquatable
{
bool IEquatable.Equals()
{
throw new NotImplementedException();
}
void IBaseInterface.BaseMethod()
{
throw new NotImplementedException();
}
}
delegate 委托
委托的使用通常需要将方法作为参数传递给其他方法时。可以将委托视为调用一个事件时会触发的方法。相当于我们不必在事件或者函数内部定义一个回调参数,而只需要把委托方法附加上去即可。
相较于接口,委托更适合灵活的方法组合,可以添加给多个事件。用我个人的理解来比喻,点餐是一个事件,假设这个饭店能APP点餐和服务员点餐,而APP只能点套餐,服务员可以一个菜一个菜给你加。现在我们分别要用委托和接口实现这个事件。委托像是有个服务员帮你点餐,你只需要说想吃什么,服务员就会帮你记下然后通知后厨。而接口像捆绑了一堆的套餐,如果点了鸡腿套餐,那一定会给你鸡腿+米饭,点了猪排套餐一定给你猪排+米饭。而使用委托,如果我们想要鸡腿+米饭,我们也可以让服务员给我们增加两个菜(方法),一个是鸡腿,一个是米饭。但是如果我们想要的菜品多,且和套餐重复的话,那么接口可以更好的实现。但如果我们想要灵活地点菜,例如我想要鸡腿+米饭,用接口实现了,然后我又想要一个猪排,如果用接口实现那接口会给我猪排+米饭。如果我只想要猪排那就应该叫服务员来,只点一个猪排。
委托可以和方法绑定,当触发委托时自动触发绑定的方法
// Declare a delegate:
delegate void Del(int x);
// Define a named method:
void DoWork(int k) { /* ... */ }
// Instantiate the delegate using the method as a parameter:
Del d = obj.DoWork;
合并委托/多路广播委托
不同的委托之间可以合并:
CustomDel hiDel, byeDel, multiDel, multiMinusHiDel;
hiDel = Hello;
byeDel = Goodbye;
multiDel = hiDel + byeDel;
multiMinusHiDel = multiDel - hiDel;
委托的合并直接使用加法即可,简单来说委托A+委托B =合并委托C,而合并委托C可以使用减法把委托A或者委托B减掉。加法就像服务员A和服务员B跟厨师说1号桌要XXX菜,2号桌要XXX菜,厨师肯定是哪桌先点餐哪桌先上菜。减法就是服务员B过来又说2号桌不要了,你别做了,那厨师只做服务员A委托的1号桌的菜就好了。
在调用多播委托时,它会按顺序调用列表中的委托。 只能合并相同类型的委托。