C#基础结构与数据类型

C#基础结构与数据类型

一、C#基本结构

C#包含以下主要结构:

命名空间 Namespace 这是用于隔开不同文件中相同变量名,当有一个项目中有多个文件或者作者时使用

主要Class 用于运行程序

Class方法 用于实现程序的功能

Class属性 给Class中添加对应内容

Main方法 C#的main函数,但是在Unity中不使用

注释用于注释

示例一:C#主要结构

//以下是主要引入内容,
//(using xxx)可以使引入`xxx`命名空间内的所有类、结构体、枚举和接口
using System;                       // 主要用来获取基础数据如(DateTime.Now)
using System.Collections;           // 用于支持协程 
using System.Collections.Generic;   // 用于支持泛型集合 
using UnityEngine;                  // 用于支持所有Unity的核心功能
using System.Text;                  // 用来高性能字符串处理
using Unity.VisualScripting;        // 用来与可视化节点联动

namespace sample    //定义命名空间
{
    public class Day9 : MonoBehaviour  //主要Class MonoBehaviour是为Unity中的物体实现功能的主要核心
                                       //Unity的固定函数,函数名一定相同才能执行
    {
        //以下是一些在Unity中默认的class方法

        // 对象在创造出来的瞬间执行
        public void Awake()
        {
            Debug.Log("Awake");
        }

        // 第一帧执行之前执行一次
        public void Start()
        {
            Debug.Log("Start");
        }

        // 每帧执行
        public void Update()
        {
            // Debug.Log("Update");
        }

        // 这个脚本被销毁的时候执行
        public void OnDestroy()
        {
            Debug.Log("OnDestroy");
        }
    }


    // C#的main函数,在Unity中用不到
    // class Program
    // {
    //     static void Main(string[] args)
    //     {
    //         Console.WriteLine("Hello");
    //     }
    // }
}

二、基础数据类型

1.基础数据类型

数据类型 描述 作用 默认值 取值范围
bool 布尔型 表示真或假 False true或false
byte 字节型 存储8 位无符号整数 0 0 到 255
char 字符型 字符类型,通常用于存储 ASCII 字符 ‘\0’ U +0000 到 U +ffff
int 整型 存储数字 0 -2,147,483,648 到 2,147,483,647
short 短整型 存储数字 0 -32,768 到 32,767
long 长整型 存储数字 0L -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
float 浮点型 单精度浮点数,存储带有小数点的数字 0.0F -3.4 x 1038 到 + 3.4 x 1038
double 双浮点型 双精度浮点数,存储带有小数点的数字 0.0D (+/-)5.0 x 10-324 到 (+/-)1.7 x 10308
void 无类型 无类型 0

2.sizeof

如需得到一个类型或一个变量在特定平台上的准确尺寸,可以使用 sizeof 。表达式 sizeof(type) 产生以字节为单位存储对象或类型的存储尺寸。

示例二:获取变量类型占用多少字节

using UnityEngine;
using System;

public class SizeTest : MonoBehaviour
{
    void Start()
    {
        Debug.Log($"int 占用: {sizeof(int)} 字节");      // 输出: 4
        Debug.Log($"float 占用: {sizeof(float)} 字节");   // 输出: 4
        Debug.Log($"double 占用: {sizeof(double)} 字节"); // 输出: 8
        Debug.Log($"bool 占用: {sizeof(bool)} 字节");     // 输出: 1
        Debug.Log($"char 占用: {sizeof(char)} 字节");     // 输出: 2 (Unicode)
    }
}

3.var

var 可以自动判断类型
例如var autoType = 10; //autoType会自动变为int

4.所有类型的父类

示例:object objectValue = IntValue;
object是所有类型(包括值类型和引用类型)的父类
任何数据类型都可以赋值给 object 类型的变量。

装箱

示例(1):装箱

//假设 IntValue 是一个 int 类型(值类型):
int IntValue = 100;
object objectValue = IntValue; // 这里发生了“装箱”

发生了什么?
1.内存迁移:int 原本存储在栈上(速度快,生命周期短)。当它被赋值给 object 时,CLR(公共语言运行时)会在堆上开辟一块新的内存。
2.复制数据:将 int 的值复制到堆上的这块新内存中。
3.包装对象:创建一个包含该值的完整对象头(包含类型信息、同步索引等),此时它不再是一个单纯的数字,而是一个 System.Int32 对象。
4.引用赋值:objectValue 这个变量存储的是堆上那个对象的引用地址(指针)。

示例:图解

栈 (Stack)                  堆 (Heap)
+----------------+          +-----------------------+
| IntValue       |          | [Object Header]       |
| (值: 100)      | -------> | [Type: System.Int32]  | <--- objectValue 指向这里
+----------------+  复制    | [Data: 100]           |
                            +-----------------------+
拆箱

如果你想把 objectValue 变回 int 进行数学运算,必须进行拆箱。

示例(2):拆箱

// 错误情况:不能直接运算
// int result = objectValue + 10; 

// 应该为显式强制转换
int backToInt = (int)objectValue; 
int result = backToInt + 10;

三、字符串

// 字符串操作简单
{
    // 字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。
    // 在对有Windows地址目录的情况会便于使用(不需要转义符)
    String sample1 = "C:\\Windows"; // 此处的(\\)中第一个(\)为转义符
    string sample2 = @"C:\Windows"; // @(逐字字符串)将转义字符(\)当作普通字符对待

    // 任意类型可以相加,只要那个类型有正确实现的ToString()
    string a = "123" + 10 + "abc" + IntValue + StringValue;

    // 获取字符串长度
    int b = a.Length;

    // 访问字符
    char c = a[1];  // 但是不能通过这个方法修改,错误的:a[1] = 'b'
                    // 字符串插值
    string d = $"IntValue的值是{IntValue}, FloatValue的值是{FloatValue}";
    // int aaa = 10;
    // UnityEngine.Debug.Log("aaa的值是: " + aaa + "。");
    // UnityEngine.Debug.Log($"aaa的值是:{aaa}。");

    // 字符串比对
    if (a.Contains("abc"))
    {
        // 判断 a 有没有包含字符串 "abc"
    }
    if (a.StartsWith("hello") || a.EndsWith("world"))
    {
        // 判断 a 的开头和结尾是不是对应字符串
    }

    // 获取某个字符串第一次出现的位置
    int f = a.IndexOf("c");

    // 截取字符串
    string g1 = a.Substring(1, 3);      // 这个3是长度,1开始包括1的后面3个,1,2,3
    string g2 = a[1..4];                // 这个4是结束下标,只有1,2,3
                                        // 分割字符串
    string h1 = "orange,banana,apple";
    string[] h2 = h1.Split(",");

    // 大小写转换
    string i = "Word";
    i = i.ToLower();
    i = i.ToUpper();

    // 显式的字符串本身也是对象,可以进行上面的所有操作
    "word".ToUpper();

    // 替换
    string l = "hello".Replace("l", "o");

    // 去空格
    string j = "  ab  cd e ".Trim();

    // 字符串内容相等比较
    if (i == j)
    {
    }

    // 字符串创建器,用于频繁加减或处理字符串
    StringBuilder stringBuilder = new();
    stringBuilder.Append("123");
    stringBuilder.AppendLine("abc");
    string k = stringBuilder.ToString();
}

四、数组

1.声明数组

主要结构

public datatype[] arrayName;
访问符 数据类型 大小 名称

// 数组

public int[] Array1; // 声明数组
public int[] Array1 = { 0, 1, 2, 3, 4, 5, 6 };// 声明含有数据的数组

实例:使用

{
Array1 = null;          // 清空数组
Array1 = new int[10];   // 初始化数组,分配内存大小
}
// 二维数组
public int[,] Array2;   // 声明矩形数组
public int[][] Array3;  // 声明交错数组

实例:使用

public void Array23()
{
    //矩形数组(在内存上是一个连续片段)
    // (想象为一个Excel表格矩阵:所有的行必须有相同的列数)
    Array2 = new int[3, 4]; // 初始化一个3行4列的矩形数组
    Array2[0, 0] = 1;       // 赋值(第0行0列)
    Array2[1, 2] = 5;       // 赋值(第1行2列)
    // 不能单独改变某一行的长度
    // Array2[0] = new int[5]; // 编译报错


    //交错数组(在内存上不连续)
    // (第一行可以有2个元素,第二行可以有100个元素,第三行可以是 null)
    // 1. 首先初始化外层数组(决定有多少行)
    Array3 = new int[3][]; 
    // 2. 然后分别初始化每一行(决定每行有多少列,可以不同!)
    Array3[0] = new int[4];    // 第0行有4个元素
    Array3[1] = new int[2];    // 第1行有2个元素
    Array3[2] = new int[100];  // 第2行有100个元素
    Array3[0][0] = 1;          // 赋值
    Array3[1][1] = 99;         // 赋值
    // 可以随时替换某一行
    Array3[1] = new int[50]; // 把第1行换成一个长度为50的新数组
}

2.数组的语法糖

// 数组的语法糖
{
    // 获取倒数第几位 Array1[Array1.Length - n]
    int a = Array1[^1];

    // 获取 index = 2, 3, 4 的数组
    int[] b = Array1[2..5];

    // 2 ~ 最后
    int[] c = Array1[2..];

    // 第一个 ~ 4
    int[] d = Array1[..5];
}

// 数组的长度不会丢失了
int length = Array1.Length;

// foreach循环
for (int i = 0; i < Array1.Length; i++)
{
}
foreach (int each in Array1)
{
}

// Array类自带大量处理函数
// Array本身也是数组的父类
{
    int[] a = { 0, 1 };
    int[] b = { 2, 3 };
    Array.Copy(a, b, 2);        // 复制
    Array.Sort(a);              // 排序
    Array.IndexOf(a, 1);        // 查找
}

// 参数数组
F3(Array1);
F3(1, 2, 3, 4, 5, 6);

public void F3(params int[] a)  // 处理多个数量的参数
                                // params用于传递任意数量的参数进入方法
                                // 传入的内容会被自动编成数组a
{
    foreach (int each in a)     // 遍历传入的数组a
    {
    }
}

// params的使用规则
// 正确方法:
//public void F3(string prefix, params int[] a) { } 

// 错误情况1:params 后面不能再有其他参数
//public void F3(params int[] a, string suffix) { } 

// 错误情况2:不能有两个 params
public void F3(params int[] a, params string[] b) { }

//params本质上还是会创建一个新的数组对象,因此如果在每帧运行的Update() 循环中会造成卡顿。

五、值类型和引用类型

C# 不通过指针来区别对象的类型,而是特定的定义类型生成特定的处理类型

值类型 -> 基础类型 + enum + struct
值类型变量可以直接分配给一个值。
值类型直接包含数据。比如 int、char、float,它们分别存储数字、字符、浮点数。
系统分配内存来存储值。

引用类型 -> class + string + 数组 + object + interface + delegate
引用类型不包含存储在变量中的实际数据,但它们包含对变量的引用。
它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。

public class  Sample : MonoBehaviour
{
    public void F4()
    {
        // 在C#中,使用class时,我们无法通过不new的方式直接分配栈内存了
        Sample day;         // 此时day == null,而不是一个对象
        day = new Sample(); // 新建day分配内存空间
        day = new();        // 如果接受的变量明确知道类型,可以不写new后面的类型

        // 值类型一定是直接复制值
        int a = 1;
        int b = 2;
        a = b;

        // 引用类型一定是修改引用(类似修改指针指向)
        Sample d1 = new();
        Sample d2 = new();
        d1 = d2; // d1 本来的内存被丢弃


        // c#拥有 垃圾回收机制(GC),不需要自己释放内存
        Sample d3 = new();
        d3 = null;

        // 任何情况下,只要 没有任何引用 指向d3的内存,d3就会被加入垃圾回收的队列
        // 比如上面
        d1 = d2;
        d2 = null; // d1 仍然指向 d2,不会被回收
    }

    public void F5(int a, Sample b)
    {
        // 同样的,函数的参数也遵守这个规则,a作为基础类型是值传递,b是class为引用传递
    }
}

六、this与引用传递

1.this

this 就是自身。
当你在一个类(Class)或结构体(Struct)的内部代码中使用 this 时,它指代的就是正在执行这段代码的那个具体对象。
核心作用是解决名字冲突
当局部变量或参数的名字和成员变量的名字一样时,编译器会优先使用局部变量。如果你想访问成员变量,就必须加上 this. 来明确告诉编译器要的是自己的变量,不是参数。

实例:this

public class  Sample : MonoBehaviour
{
    public void F1()                  //声明方法F1
    {}
    public int IntValue;              //外部变量IntValue
    IntValue = 123;                   //给IntValue赋值
    public void This()                //声明方法
    {   
        // this 代表该对象,在使用this时,能明确重名变量的定义域
        int IntValue = this.IntValue; //声明变量IntValue
        this.F1();                    //此处的 this 为 Sample
    }
}

2.引用传递

主要为函数传入外部数据内存地址

实例:引用传递

public void F7(ref int a)       //ref 传入已有数据的a内存地址
{
    a = 123;                    //可修改也可不修改
}

public void F8(out int a)       //out 传入a内存地址(由于会被强制覆盖,因此无所谓是否初始化)
{
    a = 456;                    // 如果不赋值 a 会报错
}

public class  Sample : MonoBehaviour
{
    public void Refin()
    {
    // 如果想通过函数参数直接修改外部的 值类型
    // 类似传入指针
        int d = 0; //声明d

        // ref 必须初始化,错误的:int d;(传入前没有给d任何值)
        F7(ref d);

        // out 不需要初始化,所以支持两种写法
        // 但是必须在函数内赋值
        F8(out d);
        F8(out int e);
    }
}

七、类型转换

1.隐式类型转换

将一个较小范围的数据类型转换为较大范围的数据类型

int a = 123;
float b = a;

2.显式转换

// 安全类型转换
//string str = "123";   //"123"是string类型
int c = Convert.ToInt32("123"); //转为int
//c == 123
格式说明符 描述 示例 输出
C 货币 15.ToString(“C”) ¥15.00
15.ToString(“C1”) ¥15.0
15.ToString(“C3”) ¥15.000
D 十进制数 15.ToString(“D1”) 15
15.ToString(“D2”) 15
15.ToString(“D5”) 00015
E 科学型 15.ToString(“E”) 1.500000E+001
1500.ToString(“E”) 1.500000E+003
0.15.ToString(“E”) 1.500000E-001
0.0015.ToString(“E”) 1.500000E-003
0.0015.ToString(“E0”) 2E-003
0.0015.ToString(“E1”) 1.5E-003
0.0015.ToString(“E2”) 1.50E-003
F 固定点 15.ToString(“F1”) 15.0
15.ToString(“F2”) 15.00
G 常规 2.5.ToString(“G”) 2.5
N 数字 1500000.ToString(“N”) 1,500,000.00
1500000.ToString(“N0”) 1,500,000
1500000.ToString(“N1”) 1,500,000.0
X 十六进制 15.ToString(“X”) F
15.ToString(“X3”) 00F

八、枚举

枚举是一组命名整型常量。枚举类型使用 enum 关键字声明。
C# 枚举是值类型。也就是说,枚举包含自己的值,且不能继承或传递继承。

实例八(1):枚举主要结构

enum <enum_name>
{ 
enumeration list 
};

其中
enum_name 指定枚举的类型名称
enumeration list 是一个用逗号分隔的标识符列表

实例八(2):枚举声明与使用

//如果没有显式指定值,枚举的第一个成员默认为 0,后续成员依次自动 +1。如果中间某个成员被显式赋值,后续成员会基于这个新值继续 +1
public enum ANIMAL_TYPE  // 声明枚举ANIMAL_TYPE
{
NONE,                    // 第一个,未指定值 默认是0
DOG = 3,                 // 显式指定为3 值是3
CAT,                     // 未指定值 基于上一个(DOG=3)自动+1 值是 4
};

//public enum ANIMAL_TYPE{NONE,DOG = 3,CAT};

public class Sample : MonoBehaviour
{
    public void Start()
    {
        // 枚举
        ANIMAL_TYPE t = ANIMAL_TYPE.NONE; // 使用枚举类型声明t
        if (t == ANIMAL_TYPE.DOG)         // 如果t是动物中的狗
        {
        }
    }
}

九、结构体

结构体是一种值类型,用于存储各种数据类型的相关数据。
结构体没有继承结构体的功能

实例九:结构体主要结构与使用

public struct MyVector2     // 声明结构体MyVector2
{
    public float X;         // 声明变量X
    public float Y;         // 声明变量Y
     // 也可以写函数
    public void Print()     //声明函数Print()
    {
        UnityEngine.Debug.Log($"{X}/{Y}"); //输出字符串X/Y数值
    }
}

public class Sample : MonoBehaviour
{
    public void Start()
    {
        // 结构体可以直接定义使用,但是内部变量必须初始化
        MyVector2 v1;

        // UnityEngine.Debug.Log(v1.X); // 在这行会报错
        v1.X = 2;
        UnityEngine.Debug.Log(v1.X);

        // 使用new会给所有成员变量自动初始化为默认值
        // new与不new只有是否初始化,并不影响 内存位置分配/是否是值类型 等问题
        MyVector2 v2 = new();
        UnityEngine.Debug.Log(v2.X); // 不会报错

        // v1 的成员变量只复制了 v2 的值
        v1 = v2;

        // 结构体一般用在 小数据/复制方便的数据 上
        Color color;            // 颜色
        Rect rect;              // 范围
        Quaternion quaternion;  // 四元数(Unity代表 旋转 的数据)
        Vector2 vector;         // 向量
    }
}

C#基础结构与数据类型
https://chooseqiu.com/posts/ad7c65c5/
作者
Chooseqiu
许可协议