Unity笔记:C#基础(1)

杂项

虚函数

CSDN - C++虚函数详解
cnblog - C#中的虚函数virtual

常量池与new

在C#中,string是不可变的,这意味着对string对象的操作通常会返回一个新的string对象,而不会修改原始的string对象。因此,几乎所有涉及更改string内容的方法都会返回一个新的string对象。
String s = new String("xyz")在内存中产生了多少份字符串?2个。

  1. “xyz” 字符串的常量池中的字符串对象
  2. new出来的新字符串

这种方式创建的字符串对象不会被放入常量池中,正确的操作是下面这样

string s = "xyz";

在C#中,常量池(intern pool)通常是被放置在堆中。而Substring这类操作均不会改变原来的字符串,而是创建新的。

拆箱与装箱

拆箱(Unboxing)和装箱(Boxing)是与值类型(Value Type)和引用类型(Reference Type)之间的转换相关的两个概念。

  1. 装箱(Boxing)
    • 装箱是指将值类型转换为引用类型的过程。在装箱中,值类型的实例被封装在一个对象中,并在堆上分配内存空间。
    • 例如,将一个整数值装箱为 object 类型的实例,或者将一个结构体实例装箱为 System.ValueType 类型的实例。
  2. 拆箱(Unboxing)
    • 拆箱是指将引用类型转换为值类型的过程。在拆箱中,封装在对象中的值类型实例被提取出来,放入到一个新的值类型变量中。
    • 例如,将一个装箱的整数对象拆箱为一个整数值,或者将一个装箱的结构体对象拆箱为原始的结构体实例。

装箱和拆箱操作可能会引起性能开销,因为它们涉及到数据的复制和内存分配。因此,在编写高性能的代码时应该谨慎使用。

注1:在装箱过程中,值类型的数据会被复制到堆上新分配的内存空间中,而引用会指向这个新分配的内存空间,因此装箱后的引用指向的是堆上的对象。

注2:当创建一个新的结构体时,编译器会隐式地为它添加继承自 System.ValueType 的基类,并在必要时自动实现一些与值类型相关的功能,比如装箱、拆箱等。


List会发生拆装箱吗?会,List<object>就会发生

List<object> objectList = new List<object>();
objectList.Add(20); // 添加一个整数(值类型)
objectList.Add("World"); // 添加一个字符串(引用类型)

可以使用is关键字或as关键字来检查List<object>中的某个元素的类型。从 List<object> 中取出元素时,元素的类型会被视为 object 类型,因此任何按值类型进行的操作都需要显式手动转换类型。

C#关键字

C# 中的 sealed 关键字类似于 Java 中的 final 关键字,用于类(防止继承)或方法(防止重写)

readonlyconst区别在于const是编译时常量,而readonly是运行时常量:

  1. const关键字用于声明常量,常量在声明时必须进行初始化,并且一旦初始化后,其值将无法更改。const变量在编译时会被直接替换为其值,因此它们的值必须在编译时就能确定。
  2. readonly关键字用于声明只读字段,只读字段可以在声明时或构造函数中进行初始化,一旦初始化后,其值将无法更改。与const不同,readonly字段的值是在运行时确定的,因此可以用于在构造函数中初始化。

partial关键字

partial关键字用于指示一个类、接口、结构体或方法是“部分定义”的。这意味着该类、接口、结构体或方法的定义可以分散在多个文件中。

// File1.cs
partial class MyClass
{
    public void Method1()
    {
        Console.WriteLine("Method1");
    }
}

// File2.cs
partial class MyClass
{
    public void Method2()
    {
        Console.WriteLine("Method2");
    }
}

不要试图在不同文件中重复定义某些方法或者变量,会报错。

System.Object

在C#中,所有引用类型的基类System.Object,该类实现了几个方法

在这里插入图片描述

Try

try
{
    // 可能会抛出异常的代码块
}
catch (ExceptionType1 ex)
{
    // 处理特定类型的异常
}
catch (ExceptionType2 ex)
{
    // 处理另一种类型的异常
}
finally
{
    // 无论是否发生异常,都会执行的代码块
}

如果catch后面没有括号里的条件,那就会捕获 try 块中抛出的任何类型的异常。自定义异常需要创建一个继承自 System.Exception 类的新类

using System;

// 定义自定义异常类
public class MyCustomException : Exception
{
    // 可以添加自定义的构造函数和属性
    public MyCustomException(string message) : base(message)
    {
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        try
        {
            // 在适当的情况下,抛出自定义异常
            throw new MyCustomException("This is a custom exception.");
        }
        catch (MyCustomException ex)
        {
            // 捕获并处理自定义异常
            Console.WriteLine("Custom Exception Caught: " + ex.Message);
        }
        catch (Exception ex)
        {
            // 捕获其他类型的异常
            Console.WriteLine("Exception Caught: " + ex.Message);
        }
    }
}

例如下题结果为6

int x = 0;
try
{ throw new Exception(); }
catch
{ x += 1; }
finally
{ x += 2; }
x += 3;

观察者模式、委托与Unity

CSDN - Unity中关于委托与事件的使用及区别
c# 事件和委托,再也不忘了

事件是函数的容器,类似C的函数指针但不太一样。声明事件时需要先声明一个委托类型

  • 委托通常用于实现回调函数、事件处理等场景,它可以直接被调用。
  • 事件通常用于实现发布-订阅模式,它只能在声明类的内部触发,外部无法直接调用。

一般在OnEnable()OnDisable()中注册和移除事件的订阅而非Start(),这样不会在计算机内存中留下任何无法访问的Object。事件的调用如同调用函数一般,但是在那之前需要测试事件是否为 null,只有当任何类中没有函数订阅该事件时,该事件才会为 null

委托的本质可以看作是观察者模式的一种实现方式。委托的核心是事件,用到事件的地方就可以使用委托,例如UI交互;捡到某个物品时触发一个事件,该事件将为玩家提供升级等效果;或者触发了碰撞器能够打开门;还有就是状态管理。

实际上物体的碰撞事件通常是通过委托来实现的,如OnCollisionEnterOnCollisionStayOnCollisionExit等方法

C#中的多继承、接口与虚基类

C#不直接支持多继承,一般使用接口实现类似效果。

注意在C#中,构造函数的调用顺序是由派生类向基类的方向,所以是派生类先调用基类构造函数执行,执行完才执行本类的,所以执行顺序是基类到派生类。

接口虚基类
访问控制仅publicpublic/protected/private
构造函数不能有可以有
多继承可以实现多个接口只能继承一个基类
成员定义仅定义签名无实现abstract修饰的无实现
  1. 虚类可以有抽象方法(abstract)、虚方法(virtual)和具体方法,并不一定只有前二者。
  2. 虚类通过abstract关键字指定
  3. 虚类不一定是public
  • 如果使用public修饰符,则虚类可以被任何其他类访问。
  • 如果使用protected修饰符,则虚类只能被派生类访问。
  • 如果使用internal修饰符,则虚类只能在同一程序集内部访问。
  • 如果使用private修饰符,则虚类只能在包含它的类内部访问。

这里的程序集,是指的exe或者dll,多个代码文件在构建之后组成相同的exe或者dll则认为处在一个程序集内

public abstract class MyBaseClass { ... }

内存对齐

CSDN - 【C/C++】内存对齐(超详细,看这一篇就够了)
有必要注意的是,这篇的例5讲的不太对,图也错了,我把我的理解放在了下面小节“结构体嵌套的对齐”

使用 #pragma pack(n) 指令会将结构体的对齐方式设置为 n 字节的整数倍,其中n是2的次方。例如一个大小为13的变量通常会对齐到16。

使用 #pragma pack()则取消强制对齐,恢复默认

注意:填充的缝隙也算在结构体/类的大小内,下面是个例子:

// sizeof(Base) == 8
// int4字节,bool按最大对齐
// 这里的策略是编译器在对结构体进行对齐时,按照结构体中最大的成员大小进行对齐。
class Base { int a; bool b; };

常见的对齐策略包括:

  1. 最严格对齐原则(Strictest Alignment):按照结构体中任何成员的要求,选择最严格的对齐方式。这意味着所有成员都按照自身的对齐要求进行对齐。
  2. 平均对齐原则(Average Alignment):根据结构体中所有成员的对齐要求的平均值进行对齐。这种策略可能会导致一些成员需要额外的填充来满足对齐要求。
  3. 特定对齐方式(Specified Alignment):有些编译器允许在结构体定义中指定对齐方式,例如使用 #pragma pack 或者 __attribute__((packed))。在这种情况下,结构体的对齐将根据指定的方式进行,而不是根据成员的大小。
  4. 默认对齐方式(Default Alignment):一些编译器有默认的对齐方式,可能会在不指定特定对齐方式的情况下应用。这通常会是一个合理的默认值,可以满足大多数情况下的性能和内存使用需求。

基本原则

知乎 - C/C++中内存对齐问题的一些理解
CSDN - 计算结构体大小(内存对齐原则)struct、union、class

就原则来讲,第二篇CSDN的博客是很详细的(其实很多东西我在Cppreference上没查到)

  1. 数据成员对齐规则,结构体(struct)(或联合(union))的数据成员,第一个数据成员存放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员(只要该成员有子成员,比如数组、结构体等)大小的整数倍开始(如:int 在 64bit 目标平台下占用 4Byte,则要从4的整数倍地址开始存储)
  2. 结构体的总大小,即sizeof的结果,必须是其内部最大成员长度(即前面内存对齐指令中提到的有效值)的整数倍,不足的要补齐(似乎union也需要满足这点)如果结构体A作为结构体B的成员,B的对齐大小总是按照#pragma pack(n)进行,其中n = max{A最大元素, B最大元素}

声明顺序的影响

struct st1 {
    char a[5];
    char b[3];
    int c;
};
struct st2 {
    char a[5];
    int c;
    char b[3];
};
// st1 == 12
// st2 == 16

这个归根到底是因为:相同类型的成员会连续存储在一起,不会因为对齐要求而产生间隔。数组内的n个成员视作相同类型的n个成员

struct st1 {
    int a[1];
    double p;
    char b[3];
};
struct st2 {
	char b[3];
    int a[1];
    double p;
};
// st1 == 24
// st2 == 16

类的对齐(虚函数与空类)

  1. 按照结构体对齐原则
  2. class含有成员变量和成员函数:计算大小的时候只与成员变量有关。与成员函数和静态成员无关,即普通成员函数、静态成员函数、静态成员变量。对类的大小没有影响。
  3. 虚函数对类的大小有影响,因为虚表指针的影响。在32位系统占4个字节,64位系统占8个字节。
  4. 多个虚函数也只算一个的影响。

在 C++ 中,对于空类(没有任何成员),其大小通常是 1 字节,这是因为 C++ 编译器会确保每个实例都有一个唯一的地址。

class Base{};
class Drived
{
    Base a;  // 类内没东西,按4字节对齐,大小为4
    int b;  // 一个int为4字节
};
cout << sizeof(Drived) << endl;  // 输出8

可以尝试修改Base类再输出:

// 这种情况输出也是8
class Base { int a; };
// 这种情况输出是12
class Base { int a; int b; };

在C#中,类的实例化在内存中会被对齐。即使一个类没有任何成员,它也会在内存中被对齐,其大小通常是一个指针的大小,因为每个类实例在CLR(Common Language Runtime)中都会关联一个类型对象指针。

枚举的对齐

枚举类型的对齐与其底层类型一致,在C++一般是int,但是C++11可以指定为其他合法的整数类型,如unsigned intcharshort,用法如下:

enum class MyEnum : underlying_type {
    VALUE1,
    VALUE2,
    VALUE3
};

例子如下:

enum DAY {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
// C++11新特性允许显式地指定枚举的底层类型
enum class DAY1 : char {
    MON = 'a', TUE, WED, THU, FRI, SAT, SUN
};
struct st1 {
    DAY b;
};
struct st2 {
    DAY1 b;
};
cout << sizeof(st1) << endl;	// 4
cout << sizeof(st2) << endl;	// 1

更复杂的情况,即枚举与其他的组合,就把enum当做某种整数类型计算即可。

union的大小

联合体和结构体一样,存在内存对齐

Cppreference:联合体只大到足以保有其最大成员(亦可能添加额外的尾随填充字节)。
上面的某博客:当联合体中有数组时,一方面要保证空间能够存储这个数组的大小,另一方面要保证最终的结果是最大数据类型的整数倍。

union MyUnion {
    char a[10];
    int b;
    double c;
};
// 大小输出是16,而不是10

如果加上#pragma pack(1)就是输出10了

不知道算不算参考的参考:MSDN - x64 ABI 约定概述

结构体嵌套的对齐

结合这两段代码对比:

struct stu2 {
    // size == 16
    char x;
    int y;
    char v[6];
};
struct stu1 {
    // size == 32
    char a;
    struct stu2 b;
    double f;
};
struct stu2 {
    // size == 24
    char x;
    int y;
    double z;
    char v[6];
};
struct stu1 {
    // size == 48
    char a;
    struct stu2 b;
    int c;
    int d;
    int f;
};
// 如果stu1去掉一个int,大小为40,去掉2个int大小也为40

换句话说,结构体嵌套的情况下,在默认对齐方式的情况下,总是n的整数倍,其中n = max{A中最大元素, B中最大元素}

c#的sizeof

C#无法直接使用 sizeof 操作符来获取结构体的大小

在 C# 中,sizeof 操作符用于获取未托管类型或静态成员的大小,但它不能用于获取托管类型(如结构体或类)的大小。这是因为托管类型的大小在编译时并不总是已知的,而是在运行时由 CLR (Common Language Runtime) 动态确定的。

要获取托管类型(如结构体)的大小,通常可以使用 System.Runtime.InteropServices.Marshal.SizeOf 方法,该方法在运行时动态计算类型的大小。

C#结构体布局

先看上一小节内存对齐

CSDN - C#结构体内存对齐

CSDN - C#-StructLayoutAttribute(结构体布局)

在 C# 中,结构体的布局方式可以通过 StructLayoutAttribute 特性来控制,而 LayoutKind 枚举类型用于指定这种布局方式的具体类型。

  1. Auto:自动布局。编译器根据目标平台和类型成员的排列顺序来确定结构体的布局方式
  2. Sequential:顺序布局。结构体的成员按照声明的顺序依次排列,不考虑对齐和填充。
  3. Explicit:显式布局。需要手动指定每个成员的偏移量,可以使用 FieldOffsetAttribute 特性来指定偏移量。
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
   public int x;
   public int y;
}

[StructLayout(LayoutKind.Explicit)]
public struct Rect
{
   [FieldOffset(0)] public int left;
   [FieldOffset(4)] public int top;
   [FieldOffset(8)] public int right;
   [FieldOffset(12)] public int bottom;
}

Golang选手看这个

如果是Golang选手就看这篇拿Golang讲的:CSDN - 详解内存对齐

  • 30
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值