0901学习 agile Posted on Sep 1 2023 面试 unity基础 ##C# 版本与 Unity 的关系 --- ###Unity 与 C# 版本 <table><thead><tr><th align="center">Unity 版本</th><th align="center">C# 版本</th></tr></thead><tbody><tr><td align="center">Unity 2021.2</td><td align="center">C# 9</td></tr><tr><td align="center">Unity 2020.3</td><td align="center">C# 8</td></tr><tr><td align="center">Unity 2019.4</td><td align="center">C# 7.3</td></tr><tr><td align="center">Unity 2017</td><td align="center">C# 6</td></tr><tr><td align="center">Unity 5.5</td><td align="center">C# 4</td></tr></tbody></table> --- - 不同Unity版本支持的C#版本不同,主要是不同 Unity 版本使用的C#编译器和脚本运行时版本不同 - Unity 2020.3 使用的脚本运行时版本等效于`.Net 4.6`,编译器为`Roslyn`(罗斯林编译器)。 --- ###Unity 的 .Net API 兼容级别 在 PlayerSetting `->` Other Setting `->` Api Compatibility Level 中可以设置 .Net API 的兼容级别: * .Net 4.x(特殊需求时): * 具备较为完整的 .Net API,甚至包含了一些无法跨平台的 API。 * 主要针对 Windows 平台,并且会使用到 .Net Standard 2.0 中没有的功能时,会选择使用它。 * .Net Standard 2.0(建议使用): * 是一个 .Net 标准 API 集合,相对 .Net 4.x 包含更少的内容,可以减小最终可执行文件大小。 * 具有更好的跨平台支持。 * .Net Standard 2.0 配置文件大小是. Net 4.x 配置文件的一半 --- ##typeof、GetType、is 、as --- ```C# public class A { } public class B : A { } public class C : B { } private void __PrintTypes(A e) { // e.GetType() == typeof(A):False Debug.Log($"e.GetType() == typeof(A):{e.GetType() == typeof(A)}"); // e.GetType() == typeof(B):True Debug.Log($"e.GetType() == typeof(B):{e.GetType() == typeof(B)}"); // e is A:True Debug.Log($"e is A:{e is A}"); A obj = new C(); //False Debug.Log($"{obj.GetType() == typeof(B)}"); //True Debug.Log($"{obj is B}"); var objB = obj as B; //False Debug.Log($"{objB.GetType() == typeof(B)}"); //True Debug.Log($"{objB.GetType() == typeof(C)}"); int? nullIntA = 1; int? nullIntB = null; //True Debug.Log($"{nullIntA is int?}"); //True Debug.Log($"{nullIntA is int}"); //False Debug.Log($"{nullIntB is int?}"); //False Debug.Log($"{nullIntB is int}"); } var d = new B(); __PrintTypes(d); ``` --- - `typeof(x)`:获取在编译时指定的类型名,`x`必须是具体的类名、类型名称等,绝对不可以是变量名称,不包含继承关系 - `x.GetType()`:获得一个对象在运行时的类型,不包含继承关系 - `is`:判断两个实例是否有继承关系,如果有,类型相等则返回True。运行时,包含继承关系 - `as`只能用于引用类型不能用于值类型,转换失败不会抛出异常 --- ##IsAssignableFrom和IsSubclassOf的区别 --- ```C# public interface I{} public interface II:I{} public class A : II{} public class C : B { } public class A<T>:A{} public class B<T>:A<T>{} public class D:B<int>{} //False Debug.Log(typeof(A).IsSubclassOf(typeof(I))); //False Debug.Log(typeof(II).IsSubclassOf(typeof(I))); //True Debug.Log(typeof(B).IsSubclassOf(typeof(A))); //True Debug.Log(typeof(A<>).IsSubclassOf(typeof(A))); //True Debug.Log(typeof(A<int>).IsSubclassOf(typeof(A))); //True Debug.Log(typeof(B<>).IsSubclassOf(typeof(A))); //False Debug.Log(typeof(B<>).IsSubclassOf(typeof(A<>))); //False Debug.Log(typeof(B<int>).IsSubclassOf(typeof(A<>))); //True Debug.Log(typeof(B<int>).IsSubclassOf(typeof(A<int>))); //True Debug.Log(typeof(D).IsSubclassOf(typeof(A))); //False Debug.Log(typeof(D).IsSubclassOf(typeof(A<>))); //True Debug.Log(typeof(D).IsSubclassOf(typeof(A<int>))); Debug.Log("========IsAssignableFrom======"); //True Debug.Log(typeof(I).IsAssignableFrom(typeof(A))); //True Debug.Log(typeof(I).IsAssignableFrom(typeof(II))); //True Debug.Log(typeof(A).IsAssignableFrom(typeof(B))); //True Debug.Log(typeof(A).IsAssignableFrom(typeof(A<>))); //True Debug.Log(typeof(A).IsAssignableFrom(typeof(A<int>))); //True Debug.Log(typeof(A).IsAssignableFrom(typeof(B<>))); //False Debug.Log(typeof(A<>).IsAssignableFrom(typeof(B<>))); //False Debug.Log(typeof(A<>).IsAssignableFrom(typeof(B<int>))); //True Debug.Log(typeof(A<int>).IsAssignableFrom(typeof(B<int>))); //True Debug.Log(typeof(A).IsAssignableFrom(typeof(D))); //False Debug.Log(typeof(A<>).IsAssignableFrom(typeof(D))); //True Debug.Log(typeof(A<int>).IsAssignableFrom(typeof(D))); ``` --- - `typeof(IFoo).IsAssignableFrom(typeof(BarClass))`:表示BarClass类型能否赋值给IFoo接口,所以它返回true的条件就是BarClass直接或间接实现了IFoo接口,IsAssignableFrom也可以用来判断继承关系,同时适用于接口之间的检查。 - `typeof(FooClass).IsSubclassOf(typeof(BarClass)) == true`:只能用于判断类的继承关系,不能判断是否实现某个接口 --- ##关键字const、readonly、static三者的区别 --- ###const - 编译时常量 - 属于类型级,通过类名直接访问,被所有对象共享 - 声明时常量初始化 - 只能声明为简单的数据类型(内建的int和浮点型)、枚举或字符串 - 默认为静态类型(无需用static修饰,否则将导致编译错误) - 除了可以声明为类字段之外,还可以声明为方法中的局部常量,readonly不行 --- ###readonly --- - 运行时常量 - 属于对象级,通过对象访问 - 可以被static修饰,这时的`static readonly`和`const`非常相似 - 可以在声明时初始化,也可以在构造函数里初始化(`static readonly`常量,如果在构造函数内指定初始值,则必须是`静态无参构造函数`) - 可以是任意的数据类型 - 只能用来修饰类的field,不能修饰局部变量,也不能修饰property等其他类成员 --- ###static readonly - static readonly的Reference类型,只是被限定不能进行赋值(写)操作而已。而对其成员的读写仍然是不受限制的 - 属于类型级,通过类名直接访问,被所有对象共享 - 跟const类似,但它除了在声明时初始化,还能在静态无参构造函数中初始化 --- ##ref out in --- ###in - 在泛型接口和委托的泛型参数中使用in关键字作为逆变参数,如:Action<in T> - 参数修饰符,in修饰的参数表示参数通过`引用传递`,但是参数是`只读的`,所以in修饰的参数在调用方法时`必须先初始化` - 多数情况下调用in关键字可以省略,当使用in关键字时,变量类型应与参数类型一致 - 可以使用常量作为参数,但是要求常量可以隐式转换成参数类型,编译器会生成一个临时变量来接收这个常量,然后使用这个临时变量调用方法 --- ```C# var tc = new TestClass(); var ini = 2; TestIn(in tc, ini); TestIn(tc, 2); public void TestIn(in TestClass s, in int i) { //不能修改 // i = 10; } ``` --- ###ref --- - ref和in都是`引用传递`,而且要求调用方法前需要`提前初始化`,但是与in不同的是,调用时`ref关键字不能省略`,且参数`必须是变量`,`不能是常量` - ref和out都是`引用传递`,且`在调用`时,`ref和out关键字不能省略`,且参数必须是变量,不能是常量,但是`ref要求调用方法前需要提前初始化`,且无需在调用方法结束前赋值 - 与in不同的是,在调用方法中时,可以`读写整个ref参数对象及它的成员` --- ```C# TestStruct testStruct = default; TestRef(ref testStruct); public void TestRef(ref TestStruct s) { s.Struct2 = new TestStruct2() { C = 10 }; } ``` --- ###out --- - 在泛型接口和委托的泛型参数中使用out关键字作为协变参数,如:Func<out T> - 作为参数修饰符,out修饰的参数表示参数通过`引用传递`,但是参数是必须是一个`变量`,且在`方法`中必须给这个`变量赋值`,但是在调用方法时无需初始化,也可以初始化。 --- ```C# TestOut(out var temp); public void TestOut(out TestStruct s) { s = default; } ``` --- ###引用类型参数加没加ref传参有什么区别 --- ```C# public void Test(TestClass tc) { tc = new TestClass(); } public void TestRef(ref TestClass tc) { tc = new TestClass(); } var testClass = new TestClass { a = 100 }; Test(testClass); //100 Debug.Log($"test a:{testClass.a}"); testClass.a = 20; TestRef(ref testClass); //0 Debug.Log($"testref a:{testClass.a}"); ``` --- - 引用类型并不是引用传递,而和值类型一样是值传递,不过传递的是地址 - 不加ref修饰的情况下,传递的是`testClass的引用地址,也就是堆内存地址`,可以理解成`testClass`和`tc`值一样,指向的是堆内存地址,此时重新给`tc`赋值,但`testClass`还是指向原先内存地址,所以`testClass.a`还是没有修改 - 加了ref修饰后,相当于将`testClass`传过来并取了个别名叫`tc`,自然接下来所有的改动都将直接影响`testClass` --- ##抽象类和接口的异同 --- - 都不能被实例化 - 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能被实例化 - 接口支持多继承,抽象类不能实现多继承 - 接口可以作用于`值类型`和`引用类型`,抽象类只能作用于引用类型。例如,Struct就可以继承接口,而不能继承类 - 接口只包含方法、属性、索引器、事件的签名,但不能定义字段和包含实现的方法,抽象类可以定义字段、属性、包含有实现的方法 --- ##属性和索引器的区别 --- ```C# public class A : II { public virtual int C { get; set; } public virtual int this[string s] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } } public class B : A { public override int C { get; set; } public override int this[string s] { get => base[s]; set => base[s] = value; } } ``` --- - 索引器是属性的一种,所以它实质上也是方法(函数),都不用分配内存来存储 - 主要都是用来访问其它数据成员,为它们提供读取和操作 - 索引器和属性都可以被重载,字段不能被重载 - 属性名可以自定义,索引器必须以this命名 - 属性可以是实例或静态,而索引器必须是实例 - 属性的get访问器没有参数,索引器的get访问器具有和索引器相同的形参表 - 属性的set访问器包含隐藏value参数,索引器的set访问器除了value参数外,还具有和索引器相同的形参表。 - 属性通过简单名称访问,索引器通过索引访问 --- ##`==, Equals`比较 --- ### 相等运算符`==` - 值类型,如果对象的值相等,则相等运算符 (==) 返回 true,否则返回false。(struct之间不能用==比较) - 对于`string`以外的引用类型,如果两个对象引用同一个对象,则 == 返回 true - 对于 `string` 类型,== 比较字符串的值 --- ### equals - 得看具体实现的类equal怎么实现 - `Object.Equals()`比较引用相等性,即两个变量是否指向同一个实例对象 - `String.Equals()`比较两个字符串的内容是否一致 - `struct`类型均继承自`System.ValueType`(继承自 System.Object),`System.ValueType`类型`override`了`object.Equals`方法,该`override`方法的实现会遍历value type中的每一个字段,然后在每个字段上调用各自的Equals方法。若每个字段比较的结果均相等就返回true,否则,返回false。该方法调用设计到反射,很耗性能,同时都是调用的equal(objcet)方法,也存在装箱拆箱大问题 --- ###Object的ReferenceEquals方法 - ReferenceEquals方法存在的目的是为了比较两个引用变量是否指向同一个实例对象 - 没有`override Equals`方法的类型来说,`ReferenceEquals`方法将和`Equals`方法产生相同的结果 --- ###Object的Static Equals方法 ```C# public static bool Equals(object objA, object objB) { if (objA == objB) { return true; } if (objA == null || objB == null) { return false; } return objA.Equals(objB); } ``` `static Equals`方法除了进行null检查之外,它总是和`virtual Equals`方法返回相同的结果 --- ###`==和继承性问题` ```C# var str1 = "Hello"; var str2 = string.Copy(str1); //True Debug.Log(str1 == str2); //True Debug.Log(str1.Equals(str2)); //False Debug.Log(object.ReferenceEquals(str1, str2)); //True Debug.Log(object.Equals(str1, str2)); object str3 = str1; object str4 = str2; //False Debug.Log(str3 == str4); //True Debug.Log(str3.Equals(str4)); //False Debug.Log(object.ReferenceEquals(str3, str4)); //True Debug.Log(object.Equals(str3, str4)); ``` --- - 上面的结果进行比较,可以发现,只有==操作符的结果发生了改变 - 就在于`==`相当于一个`静态方法`,而静态方法不可能是`virtual`的,本例中当用==进行比较时,比较的是两个Object类型的变量,尽管我们知道str和str1实际是String类型的,但编译器并没有意识到这一点 - 对于`非虚方法`的调用而言,具体调用哪个实现是在`编译时期`就已经做出决定了 - 我们声明了`Object`类型的两个变量`str3和str4`,那么编译器就会生成`比较Object类型的代码`。而`Object类中是没有==操作符的重载版本实现`的,因此,`==将进行引用相等性比较`,因str3和str4是两个不同的实例对象,故返回False --- ###==和泛型问题 ```C# private bool __CompareTest<T>(T v1, T v2) { // 因为T可能代表任意类型,包括基元类型、值类型和引用类型。无法确定传递的类型是否实现了==操作符重载 // return v1 == v2; //这样也可以 //return object.Equals(v1,v2); //最好这样子,同事要求该类型T实现IEquatable接口 return EqualityComparer<T>.Default.Equals(v1, v2); } //如果限定T为class,这样其实是可以比较的 private bool __CompareTest<T>(T v1, T v2) where T : class { return v1 == v2; } ``` 在对泛型类型T使用==操作符时,需要限定为class类型,或者是用object.Equals比较,但对于值类型会有装箱拆箱问题,所以推荐用EqualityComparer比较 --- ###判断泛型的值是否为default(T) ```C# public static bool IsDefault<T>(this T value) { // 如果用==直接判断(default(T) == value), // 编译时会提示错误:Error CS0019: 运算符“==”无法应用于“T”和“T”类型的操作数 // if (default(T) == value) // { // // } // object.Equals的问题 // object提供了一个静态方法,可用于比较两个对象是否相等: // 但是该方法接收的是引用类型的实例,如果传入的是值类型(譬如int、enum、struct等), // 则会对值类型进行装箱(boxing)。 // 其实现了一个默认的比较器,可以比较Byte、Nullable<T>、Enum、Object、 // 以及实现了IEquatable<T>接口(譬如int、bool、自定义类型等)的所有类型 return EqualityComparer<T>.Default.Equals(value, default(T)); } ``` --- ###总结 - 对于`基元类型`,比如int, float, long, bool等,`两者均比较值`,故比较结果一致。 - 对于`大多数引用类型`而言,`==`和`Object.Equals`方法均默认比较引用,但可以选择`重载==和Equals`方法,但为了不使该类型的使用者感到困惑,应同时overload或override两者,并使两者的判定结果保持一致。 - 对于`非基元值类`型而言,`Object.Equals`方法将通过`反射`进行`值相等性测试`,但它的性能低下,实践中最好override该方法以实现快速判等;而`==操作符`默认情况下`不可用`,若想用需自己实现 --- ###关于Unity null check的误区 ```C# private IEnumerator Test() { Object obj = new GameObject(); Destroy(obj); yield return new WaitForEndOfFrame(); //需要等一帧才能销毁 //True //表示c++指针为空 Debug.Log(obj == null); //False //gc还没有回收该obj,也就是说c#端其实还没回收 Debug.Log((object)obj == null); // 使用未重载的“==”进行比较,不会创建新的对象 obj = obj ?? new GameObject(); //True Debug.Log(obj == null); //False Debug.Log((object)obj == null); // 使用重载过的“==”进行比较,结果为true if (obj == null) { obj = new GameObject(); } //False Debug.Log(obj == null); //False Debug.Log((object)obj == null); } ``` - 不要使用`??`,`?.`语法糖,直接使用`==`或者`!=`进行判断 - 当`Destroy`方法被调用来销毁一个物体时,这个物体在`C++`侧存储的数据会被`销毁`,这个物体上的C#组件将被移除,但这些组件仍会被`C#侧`持有,直到`C#触发gc销毁`这些组件。(实际上Destroy被调用时只是给object设置了一个销毁标记,真正的销毁操作要等到update之后render之前。所以想要知道一个object是否被销毁要在update之后进行判断) - 当一个`C++object`被销毁以后,又去访问这个`object`,此时由于C#中的object还`没有被销毁`Unity就返回了一个`伪空对象` - 所有的`UnityEngine.Object`都对`==`操作符进行了重载,当我们使用`==`进行null检测时,表达式的含义就不再是`对象是否为null`,而是`对象是否被销毁` - `手动`将UnityEngine.Object赋值为`null`可以使对象从`fake null`状态进入`真正的null`状态 - 可以通过将对象`强转为System.Object`来查看其是否真的是`null` - 如果目的是简单地检查`monoBehaviour 变量是否已正确初始化与引用`,推荐使用显式调用 ```C# if (!object.ReferenceEquals(monoBehaviour, null)) monoBehaviour.Invoke("Attack", 1.0f); ``` - 如果目的是`检查底层引擎对象的生命周期`,推荐使用显式的 null 或 boolean 比较 ```C# if (monoBehaviour != null) monoBehaviour.Invoke("Attack", 1.0f); // 也可使用隐式的 bool 转换运算符 if (otherBehaviour) otherBehaviour.Invoke("Attack", 1.0f); ``` --- ##`IEquatable`,`IEqualityComparer`接口 --- ```C# public interface IEquatable<T> { bool Equals(T other); } public interface IEqualityComparer<in T> { bool Equals(T x, T y); int GetHashCode(T obj); } ``` - `IEqualityComparer`是一个接口,通常用于为`集合类(如字典和哈希集合)提供自定义的相等性比较`逻辑 它允许你为特定集合类提供自定义的比较规则,例如在字典中查找键时,或者在哈希集合中判断是否包含某个元素。 - `IEquatable`也是一个接口,主要用于`类型自身`的`相等性比较`,而不是集合中的对象比较。它允许你为某个自定义类型定义自己的相等性比较规则。 - 如果添加到字典中的类或者struct实现了IEquatable该接口,则`IEqualityComparer`接口的比较过程中就会调用`IEquatable`实现的方法 --- - 推荐自己创建的struct类型都实现该接口,否则添加到list,dic可能出现GC情况 ```C# var cache = new Dictionary<TestStruct, int>(); cache.Add(new TestStruct(), 1); //Dictionary代码 //在add进dictionary的时候会进行比较操作 ... IEqualityComparer<TKey> comparer = _comparer; ... if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) //如果没有提供自己大比较方法,那基本上就用了默认比较方法defaultComparer,如果是struct类型,可能就会走到这逻辑 public override bool Equals(T x, T y) { if (x != null) { //走如下的代码 if (y != null) return x.Equals(y); return false; } if (y != null) return false; return true; } [Pure] public override int GetHashCode(T obj) { if (obj == null) return 0; return obj.GetHashCode(); } //如果没有实现IEquatable接口 就会走ValueType下的 public override bool Equals (object obj) { return DefaultEquals (this, obj); } 就会出现装箱情况出现 ``` - 如果是`值类型`的数据同样会发生`装箱`,因此包括`Stack、Queue、List`等在内的数据结构由于在构造器阶段`无法传入EqualityComparer`,所以如果将未实现`IEquatable的struct作为T`的话就必定会在查找索引时产生GC - 如果使用未实现`IEquatable的struct`创建一个Array时,一旦调用IndexOf一类的方法时也会产生GC - 对于`struct类型`,如果是第三方的无法修改,只能同样以第一种创建`IEqualityComparer`的方式来优化,如果是自定义的可以修改的struct,则可以通过直接实现IEquatable接口来避免GC。 - `Enum`类型建DefaultComparer时针对enum类型做了新的处理,最好确定下版本查看`EqualityComparer<T> CreateComparer()`这个方法有没有具体特殊处理 - 对于其他值类型,系统都实现了`IEquatable`这个接口,所以推荐是自己实现大struct也实现这个接口 --- ##`IComparable`,`IComparer`接口 ```C# public interface IComparable<in T> { int CompareTo(T other); } public interface IComparer<in T> { int Compare(T x, T y); } ``` - `IComparable`:用于`定义一个对象`的自然排序顺序,通常用于对对象进行比较和排序操作。它表示对象可以自己比较自己与另一个对象的大小关系。 - `IComparable`:该方法`返回值`为`负数`表示`当前对象小于参数对象`,返回值为`零`表示`两者相等`,返回值为`正数`表示`当前对象大于参数对象`。 - `IComparer`:为对象定义一个独立的`比较器类`,该类负责比较`两个对象`的大小关系,通常用于提供多种不同的比较规则,而`不修改对象本身` - 如果你将自定义类型存储`List<T>`、`SortedSet<T>`中并希望对其进行排序,你可以通过实现 IComparable<T> 来定义它们的自然排序规则 - 有时你需要在不修改集合中的元素本身的情况下,提供不同的排序规则。在这种情况下,你可以通过将实现了 IComparer<T> 接口的比较器传递给集合类的排序方法来实现自定义排序。 --- ```C# public class PersonAgeComparer : IComparer<Person> { public int Compare(Person x, Person y) { return x.Age.CompareTo(y.Age); } } public class Person : IComparable<Person> { public string Name { get; set; } public int Age { get; set; } public int CompareTo(Person other) { return Age.CompareTo(other.Age); } } ``` - 在List<T>等集合类中,需要排序,如果T是struct类型,也需要注意排序的类型是否实现了`IComparable`接口,如果没实现该接口很可能会有GC产生 ```C# internal class ObjectComparer<T> : Comparer<T> { public override int Compare(T x, T y) { return //public int Compare(Object a, Object b) System.Collections.Comparer.Default.Compare(x, y); } } ``` - 如果不好修改子类型,也可以自己实现`IComparer`,不走默认的Comparer,避免GC的产生 --- ###集合相关接口 --- - `IEnumerable`:如果需要自定义的类型支持foreach 语义,就需要继承这个接口。有泛型版本(支持协变)。 - `IEnumerator`:所有泛型计数器(我习惯叫他们迭代器)的基类,提供了一个方法,能够迭代到集合当前的元素。有泛型版本(支持协变)。 - `IComparer`:用以对比两个对象 x,y,如果 `x>y` 返回一个大于 0 的整数,如果 `x<y` 返回一个小于 0 的整数,如果 `x=y` 返回一个等于 0 的整数。会被 `Sort`和`BinarySearch`调用。 - `IEqualityComparer`:通常用于对比集合内部两个对象是否相等。返回一个`bool`变量。有泛型版本(支持逆变)。 - `ICollection`:继承自 `IEnumerable`。所有集合类型的基础接口。定义迭代器,大小,以及用于同步的方法。有泛型版本。 - `KeyValuePairs`:键值对类。有泛型版本。其中的 TKey 和 TValue 可以为任意类型。 - `IList`:所有泛型集合的基类。有泛型版本。 - `IDictionary`:所有泛型字典基础接口。有泛型版本。 --- 0908学习 0831学习