拒绝背话术,帮助你深刻理解其原理,真正做到实战时游刃有余!适用于Unity游戏开发、C#语言相关面试等相关学习。
此为C#初级篇,不久将更新后续篇目。
1、初始化位置不同。const必须在声明的同时赋值;readonly即可以在声明处赋值,也可以构造
⽅法⾥赋值。
2、修饰对象不同。const可以修饰类的字段,也可以修饰局部变量;readonly只能修饰类的字段
3、const是编译时常量,在编译时确定该值;readonly是运⾏时常量,在运⾏时确定该值。
4、const默认是静态的;⽽readonly如果设置成静态需要显示声明
5、修饰引⽤类型时不同,const只能修饰string或值为null的其他引⽤类型;readonly可以是任何类型。
const
:
const
用于定义编译时常量,在编译时就必须为其赋值,且赋值后在整个程序运行期间都不能再修改。int
、double
、bool
等)、枚举类型或字符串类型来定义常量。public class ExampleClass
{
public const int ConstantValue = 10;
}
readonly
:
readonly
用于定义运行时常量,它可以在声明时初始化,也可以在构造函数中进行初始化。一旦初始化完成,其值就不能再被修改。public class ExampleClass
{
public readonly int ReadonlyValue;
public ExampleClass(int value)
{
ReadonlyValue = value;
}
}
const
:
const
常量在编译时会被直接替换为其对应的值,因此不会在运行时分配额外的内存空间。例如,当代码中使用const
常量时,编译器会将其替换为具体的常量值,而不是通过变量引用。const int a = 5;
int result = a + 3; // 编译时,a 会被直接替换为 5
readonly
:
readonly
变量在运行时分配内存空间,其值在初始化后存储在该内存中。每次访问readonly
变量时,都会通过该内存地址来获取其值。readonly int b = 5;
int result = b + 3; // 运行时通过内存地址访问 b 的值
const
:
const
成员隐式地为静态成员,不能使用static
关键字来修饰const
常量。可以通过类名直接访问const
常量,无需创建类的实例。public class MyClass
{
public const string MyConstant = "Hello";
}
// 直接通过类名访问
string value = MyClass.MyConstant;
readonly
:
readonly
可以是实例成员,也可以是静态成员。当作为实例成员时,每个类的实例都可以有不同的readonly
值;当使用static
关键字修饰时,它成为静态成员,可通过类名直接访问。public class MyClass
{
public readonly int InstanceReadonly;
public static readonly int StaticReadonly = 20;
public MyClass(int value)
{
InstanceReadonly = value;
}
}
// 实例成员访问
MyClass obj = new MyClass(10);
int instanceValue = obj.InstanceReadonly;
// 静态成员访问
int staticValue = MyClass.StaticReadonly;
const
:
const
是编译时常量,在子类中不能重写父类的const
成员。如果子类中定义了与父类同名的const
常量,它们是完全独立的两个常量。class ParentClass
{
public const int MyConst = 10;
}
class ChildClass : ParentClass
{
public const int MyConst = 20; // 与父类的 MyConst 是不同的常量
}
readonly
:
readonly
成员,每个实例可以有不同的初始值,表现出一定的灵活性。而静态readonly
成员在类的所有实例间共享同一个值。在继承体系中,子类不能重写父类的readonly
成员,但可以在子类中定义自己的readonly
成员。class ParentClass
{
public readonly int MyReadonly = 10;
}
class ChildClass : ParentClass
{
public readonly int MyReadonly = 20; // 子类自己的 readonly 成员
}
值类型:包含了所有简单类型(整数、浮点、bool、char)、struct、enum。
继承自System.ValueType
引用类型:包含了string,object,class,interface,delegate,array
继承自System.Object
ArrayList 不带泛型
List 带泛型
ArrayList 需要装箱拆箱 List不需要
ArrayList存在不安全类型(ArrayList会把所有插⼊其中的数据都当做Object来处理)装箱拆箱的 操作(费时)IList是接⼝,ArrayList是⼀个实现了 该接⼝的类,可以被实例化
List类是ArrayList类的泛型等效类。它的大部分用法都与ArrayList相似,因为List类也继承了IList接口。最关键的区别在于,在声明List集合时,我们同时需要为其声明List集合内数据的对象类型。
详解:C#面试常考随笔6:ArrayList和 List的主要区别?-CSDN博客
重载发生在同类中,重写在父子类中。
定义方式不同,重载方法名相同参数列表不同,重写方法名和参数列表都相同。
调用方式不同,重载使用相同对象以不同参数调用,重写用不同对象以相同参数调用。
多态时机不同,重载时编译时多态,重写是运行时多态。
【运行时多态主要通过方法重写和虚方法(使用virtual
和override
关键字)来实现】
List它的底层实现是使用数组。List类中的元素可以通过索引来访问,并且在索引越界时会抛出异常。
当我们对List中的元素进行增加操作时(add),List会自动重新分配内存以适应变化后的大小。List底层的数组会在元素数量不够时扩容。所以最好是一开始就初始化好数量,而不是一个一个add进去。
具体扩容:当向列表中添加元素并且列表的容量不足以容纳新元素时,List
会进行扩容。扩容操作通常会将数组的大小增加一倍(尽管这不是固定的,并且可能因实现而异),并将现有元素复制到新的数组中。扩容操作的时间复杂度是 O(n),但由于它只在需要时发生,因此平均下来对性能的影响较小。
List
:基于动态数组实现。它在内存中是连续存储元素的,当元素数量超过当前数组容量时,会创建一个更大的新数组,并将原数组元素复制到新数组中。LinkedList
:基于双向链表实现。链表由一系列节点组成,每个节点包含一个元素以及指向前一个节点和后一个节点的引用,元素在内存中并非连续存储。List
:支持随机访问,通过索引可以在O(1)的时间复杂度内访问任意位置的元素。LinkedList
:不支持随机访问。要访问特定位置的元素,必须从链表的头部或尾部开始,逐个遍历节点,时间复杂度为O(n) 。例如:using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
LinkedList linkedList = new LinkedList(new[] { 1, 2, 3, 4, 5 });
int index = 2;
LinkedListNode current = linkedList.First;
for (int i = 0; i < index; i++)
{
current = current.Next;
}
int element = current.Value;
Console.WriteLine(element);
}
}
List
:
list.Insert(0, 0);
LinkedList
:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
LinkedList linkedList = new LinkedList(new[] { 1, 2, 3 });
LinkedListNode node = linkedList.Find(2);
linkedList.AddBefore(node, 10);
}
}
List
:由于是连续存储,内存空间相对紧凑,但扩容时可能会有一定的内存浪费,因为新数组的容量通常会比原数组大。LinkedList
:每个节点除了存储元素外,还需要额外的空间来存储前后节点的引用,因此内存开销相对较大。List
:适用于需要频繁随机访问元素,且插入、删除操作主要在列表末尾进行的场景,例如需要按索引快速查找元素的情况。LinkedList
:适用于需要频繁在列表中间或开头进行插入、删除操作,而对随机访问需求较少的场景,例如实现队列、栈等数据结构。虽然在调用时候,下方两个例子看起来写法一样,好像都是间接实例化,那是否可以认为接口可以间接实例话呢?答案是不能的,因为:
InterfaceB interfaceB = new MyClass();
FatherClass B = new MyClass();
抽象类虽然不能直接实例化,但它有构造函数等类的特性。当创建继承自抽象类的子类对象时,子类构造函数会先调用抽象类的构造函数来完成父类(抽象类)的初始化等操作,这相当于间接地实例化了抽象类。抽象类可以有非抽象方法和成员变量等具体实现部分,子类继承抽象类并实现其抽象方法,从而完成抽象类的 “间接实例化”,可以将抽象类看作是一个不完整的类,子类来补充完整。
接口是完全抽象的,它没有构造函数,也没有成员变量(只有被public static final
修饰的常量)和非抽象方法,仅仅是方法签名的集合,定义了一种契约。类实现接口只是承诺实现接口中定义的方法。“InterfaceB interfaceB = new MyClass();
” 只是接口类型的引用指向了实现类的实例,是多态的体现,接口本身并没有被实例化。接口只是定义了规范,实现类按照规范去实现方法,不存在像抽象类那样在子类实例化过程中对自身的初始化等类似实例化的行为。
不能。
base
关键字在子类构造函数中显式调用父类的特定构造函数,但这并不是重写构造函数,而是在子类构造函数执行前执行父类构造函数的初始化逻辑。1.静态成员⽤statis修饰符声明,类被第一次加载到内存时创建,通过类进⾏访问
2.不带static的变量时⾮静态变量,在对象被实例化时创建,通过对象进⾏访问,
3.静态⽅法⾥不能使⽤⾮静态成员,⾮静态⽅法可以使⽤静态成员
4.静态成员属于类,⽽不属于对象
可以在方法调用时候,将参数按引用传递参数
赋值上:
ref:传递前必须已经初始化。方法内部可以访问、修改。
out:传递前可以不用初始化。在方法内部传出值。
参数传递方向:
ref:主要是将数据传入方法,也可以传出。
out:将方法内的数据传出。内部会忽略传入的值。
RefMethod(ref num1);
OutMethod(out num2);
void RefMethod(ref int number)
{
number += 5;
}
void OutMethod(out int number)
{
number = 20;
}
string.Empty相当于“”,Empty是⼀个静态只读的字段。 string str="" ,初始化对象,并分配⼀个空字符
串的内存空间 string str=null,初始化对象,不会分配内存空间
C# 中,由于string
类型是不可变的,当在函数中多次使用+=
操作符来拼接字符串时,每次操作都会创建一个新的string
对象,旧的对象则成为垃圾对象,这会导致大量的内存分配和垃圾回收,产生内存垃圾和碎片。
在需要多次拼接字符串的场景中,优先使用StringBuilder
类
详解:
C#常考随笔2:函数中多次使用string的+=处理,为什么会产生大量内存垃圾(垃圾碎片),有什么好的方法可以解决?-CSDN博客
创建了两个对象。
在常量池创建字符串“xyz”。堆中创建一个变量s。
String是字符串常量,线程安全。
StringBuilder是字符串变量,线程不安全。
String类型是个不可变的对象,当每次对String进⾏改变时都需要⽣成⼀个新的String对象,然后将指针指向⼀个新的对象,如果在⼀个循环⾥⾯,不断的改变⼀个对象,就要不断的⽣成新的对象,所以效率很低,建议在不断更改String对象的地⽅不要使⽤String类型。
StringBuilder对象在做字符串连接操作时是在原来的字符串上进⾏修改,改善了性能,相当于字符数组。连接操作频繁的时候,使⽤StringBuilder对象。
一般不对,有相同的hashcode。(Equals默认情况是比较两个对象引用是否相等,也就是是否指向同一个对象实例)
(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
(2)如果两个对象的hashCode相同,它们并不一定相同。
详解:C#常考随笔3:对象比较obj1.Equals(obj2)== true时候,hashcode是否相同?-CSDN博客
<<:左移,就是乘2。2<<4就是2的4次方
>>:右移,就是整除2。a >>= 1 等同
a &1 == 1:按位与,判断a是奇数还是偶数,等同a%2 == 1
int?为可空类型,默认值可以是null
int默认值是0
int?
:当数据可能存在缺失或未知的情况时,使用int?
。例如,在数据库中,如果一个字段允许为null
,在 C# 中读取该字段的值时,就可以使用int?
类型来接收。
详解:
C#面试常考随笔4:int? 和 int的区别,以及int?的运用场景?-CSDN博客
反射:
运行时候动态获取类型的信息(方法、属性等),并可以动态地调用这些成员、创建对象等。
在性能方面,涉及到运行时的类型检查和动态调用,性能相对较低。
运用场景:
对象的数据映射和转化、配置文件驱动对象的创建、主程序动态加载插件
使用示例:C#面试常考随笔5:简单讲述下反射-CSDN博客
序列化:
序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以存储和传输数据。
就像保存xml、二进制等文件。
foreach只读,不能修改元素、集合和大小
简洁易读,一定程度上减少出错
sealed
修饰符主要用于类和方法,简单来说就是让类不能继承,或者方法、属性不能重写。有助于保证代码的安全性和可维护性。
当 sealed
修饰符用于类时,该类被称为密封类,它具有以下特点:
密封类不能作为基类被其他类继承,这有助于防止类被意外扩展,保证类的实现细节不被修改。例如:
sealed class SealedClass
{
public void Method()
{
Console.WriteLine("这是密封类的方法");
}
}
// 以下代码会报错,因为不能继承密封类
// class DerivedClass : SealedClass
// {
// }
由于密封类不能被继承,编译器可以进行一些优化,因为它不需要考虑派生类可能带来的影响,从而提高运行时的性能。在某些情况下,尤其是在频繁调用密封类的方法时,性能提升可能会比较明显。
使用 sealed
修饰类可以清晰地表达开发者的设计意图,表明这个类的设计已经完整,不希望被其他开发者进行扩展。
当 sealed
修饰符用于方法时,该方法必须是重写基类的虚方法,它具有以下特点:
如果一个方法被声明为 sealed
,那么在派生类中就不能再对该方法进行重写。这在需要控制方法重写的层次时非常有用。例如:
class BaseClass
{
public virtual void VirtualMethod()
{
Console.WriteLine("基类的虚方法");
}
}
class DerivedClass : BaseClass
{
public sealed override void VirtualMethod()
{
Console.WriteLine("派生类中密封的重写方法");
}
}
// 以下代码会报错,因为不能重写密封方法
// class FurtherDerivedClass : DerivedClass
// {
// public override void VirtualMethod()
// {
// Console.WriteLine("尝试重写密封方法");
// }
// }
sealed
修饰符只能用于重写的虚方法,对于非虚方法或未重写的方法,不能使用 sealed
修饰。这意味着在使用 sealed
修饰方法时,必须有一个基类的虚方法作为基础。
sealed
也可以用于属性,和用于方法类似,它只能应用于重写的属性,目的也是阻止派生类进一步重写该属性。例如:
class BaseClass
{
public virtual int MyProperty
{
get { return 0; }
}
}
class DerivedClass : BaseClass
{
public sealed override int MyProperty
{
get { return 1; }
}
}
DerivedClass
重写了 BaseClass
的 MyProperty
属性并将其密封,派生自 DerivedClass
的类就不能再重写该属性。