Skip to content

C# 赋值运算符与 sizeof 运算符:从变量贴标签到内存量尺寸 ​

开篇:程序的「贴标签」与「量尺寸」 ​

想象你有一排储物柜。往柜子里放东西,然后在柜门上贴一张标签——这就是赋值运算符干的事:把值「贴」到变量上。而当你想知道某个柜子占多大空间时,你拿一把尺子去量——这就是 sizeof 运算符干的事:测量类型在内存中占用的字节数。

C# 中的赋值运算符家族庞大(从基础的 = 到复合的 +=、位运算的 &=,再到空合并的 ??=),而 sizeof 则是一个「量尺寸」的特殊工具,但在安全上下文中受到严格限制。掌握它们,你能写出更简洁的赋值代码,也能在需要精确控制内存时游刃有余。


第一模块:赋值运算符 ​

一、基础赋值运算符 =:贴标签的艺术 ​

1.1 什么是赋值? ​

赋值运算符 = 将右边的值存储到左边的变量中。方向永远是从右到左,像贴标签一样——先有东西,再有标签。

csharp
// 基础语法:变量 = 值;
int age = 25;           // 把 25 贴到 age 这个标签上
string name = "张三";    // 把 "张三" 贴到 name 标签上
bool isActive = true;   // 把 true 贴到 isActive 标签上

// 赋值后,变量就"代表"了这个值
Console.WriteLine(age);      // 输出:25
Console.WriteLine(name);     // 输出:张三
Console.WriteLine(isActive); // 输出:True

关键认知:= 不是「等于」,而是「把右边给左边」。判断相等要用 ==。

csharp
int x = 5;
int y = 10;

// ❌ 常见错误:把赋值当比较
// if (x = y) { }  // 编译错误(C# 不允许在条件中直接赋值,防止误写)

// ✅ 正确:比较用 ==,赋值用 =
if (x == y) { Console.WriteLine("相等"); }  // false,不输出
x = y;  // 现在 x 也变成 10 了
Console.WriteLine(x);  // 输出:10

1.2 多重赋值:一行贴多个标签 ​

csharp
// 链式赋值:从右到左依次执行
int a, b, c;
a = b = c = 100;  // 先 c = 100,然后 b = c,最后 a = b
Console.WriteLine($"a={a}, b={b}, c={c}");  // 输出:a=100, b=100, c=100

// 注意:链式赋值中,所有变量共享同一个值
// 但如果是引用类型,要小心共享引用的问题

二、复合赋值运算符:先运算,再贴标签 ​

复合赋值运算符是「先对变量做运算,再把结果贴回去」的简写形式。它们把 x = x + 5 压缩成 x += 5。

生活类比:你有一个存钱罐,复合赋值就像「先往里面加钱,再盖上盖子」——+= 是加钱,-= 是取钱,*= 是让钱生利息。

2.1 算术复合赋值:+=、-=、*=、/=、%= ​

csharp
int score = 100;

score += 20;   // 等价于 score = score + 20;  → 120
score -= 10;   // 等价于 score = score - 10;  → 110
score *= 2;    // 等价于 score = score * 2;   → 220
score /= 5;    // 等价于 score = score / 5;   → 44
score %= 7;    // 等价于 score = score % 7;   → 2

Console.WriteLine(score);  // 输出:2

字符串的 +=:拼接的快捷方式

csharp
string message = "Hello";
message += " ";      // message = "Hello "
message += "World";  // message = "Hello World"
message += "!";      // message = "Hello World!"
Console.WriteLine(message);  // 输出:Hello World!

// ⚠️ 注意:字符串 += 会创建新字符串(字符串不可变)
// 频繁拼接建议用 StringBuilder

2.2 位运算复合赋值:&=、|=、^=、<<=、>>= ​

位运算是对二进制位进行操作,复合赋值让代码更紧凑。

csharp
byte flags = 0b_0000_0000;  // 8 位二进制,初始全 0

// |= 按位或:把某一位设为 1
flags |= 0b_0000_0001;  // 第 0 位设为 1 → 0b_0000_0001
flags |= 0b_0000_0100;  // 第 2 位设为 1 → 0b_0000_0101

// &= 按位与:只保留某一位(掩码操作)
flags &= 0b_0000_0011;  // 只保留低 2 位 → 0b_0000_0001

// ^= 按位异或:翻转某一位(相同为0,不同为1)
flags ^= 0b_0000_0001;  // 翻转第 0 位 → 0b_0000_0000

// <<= 左移:所有位向左移,右边补 0
flags = 0b_0000_0001;
flags <<= 2;            // 左移 2 位 → 0b_0000_0100(相当于 ×4)

// >>= 右移:所有位向右移,左边补符号位(或补 0)
flags >>= 1;            // 右移 1 位 → 0b_0000_0010(相当于 ÷2)

Console.WriteLine(Convert.ToString(flags, 2).PadLeft(8, '0'));  // 输出:00000010

实战场景:用位运算管理权限开关

csharp
// 用每一位代表一个权限开关
const byte ReadPermission    = 0b_0000_0001;  // 第 0 位:读取
const byte WritePermission   = 0b_0000_0010;  // 第 1 位:写入
const byte ExecutePermission = 0b_0000_0100;  // 第 2 位:执行

byte userPermissions = 0;

// 授予权限:用 |=
userPermissions |= ReadPermission;      // 0b_0000_0001
userPermissions |= WritePermission;     // 0b_0000_0011

// 检查权限:用 & 后判断是否为 0
bool canRead = (userPermissions & ReadPermission) != 0;   // true
bool canExecute = (userPermissions & ExecutePermission) != 0;  // false

// 撤销权限:用 &= ~(按位与取反)
userPermissions &= (byte)~WritePermission;  // 0b_0000_0001

Console.WriteLine($"可读取:{canRead}, 可执行:{canExecute}");
// 输出:可读取:True, 可执行:False

2.3 复合赋值的「隐式转换」陷阱 ​

csharp
// ⚠️ 陷阱:复合赋值会自动进行隐式转换
byte small = 100;

// small += 200;  // 编译错误!为什么?
// 因为 small + 200 是 int 类型,byte 无法直接接收 int

// ✅ 解决1:显式强制转换
small += (byte)200;  // 溢出!300 超出 byte 范围(0-255)

// ✅ 解决2:先扩大类型再计算
byte result = (byte)(small + 200);  // 结果:44(300 % 256 = 44)

// ✅ 解决3:用更大的类型接收
int bigger = small + 200;  // 结果:300

规则:x += y 等价于 x = (T)(x + y),其中 T 是 x 的类型。编译器会自动插入强制转换,但溢出风险由你承担。


三、空合并赋值运算符 ??=:为 null 时贴标签 ​

??= 是 C# 8.0 引入的「如果左边是 null,就把右边贴上去」的运算符。

生活类比:你有一个备用钥匙盒,??= 就像「如果盒子里没钥匙,就放一把进去」。

csharp
string userName = null;

// 传统写法:繁琐
if (userName == null)
{
    userName = "默认用户";
}

// ??= 写法:一行搞定
userName ??= "默认用户";
Console.WriteLine(userName);  // 输出:默认用户

// 第二次执行:userName 已非 null,不再赋值
userName ??= "另一个名字";
Console.WriteLine(userName);  // 输出:默认用户(没变!)

底层逻辑:x ??= y 等价于 x = x ?? y,即「如果 x 为 null 则用 y,否则保持原值」。

csharp
// 实战:延迟初始化缓存
private Dictionary<string, string> _cache;

public Dictionary<string, string> Cache => _cache ??= new Dictionary<string, string>();

// 等价于:
// public Dictionary<string, string> Cache
// {
//     get
//     {
//         if (_cache == null)
//             _cache = new Dictionary<string, string>();
//         return _cache;
//     }
// }

与三元运算符的对比:

csharp
string a = null;

// 三元运算符:每次都会判断
string result1 = a != null ? a : "默认值";

// ??=:更简洁,且只执行一次赋值
a ??= "默认值";
string result2 = a;

四、交错赋值:一行代码,多个变量 ​

交错赋值(Deconstruction)是 C# 7.0+ 的特性,用元组语法同时给多个变量赋值。

生活类比:搬家时把「客厅的东西放左边,卧室的东西放右边」——一次搬运,两处安放。

csharp
// 基础交错赋值
(int x, int y) = (10, 20);
Console.WriteLine($"x={x}, y={y}");  // 输出:x=10, y=20

// 交换两个变量的值(无需临时变量!)
int a = 5, b = 10;
(a, b) = (b, a);
Console.WriteLine($"a={a}, b={b}");  // 输出:a=10, b=5

// 从方法返回多个值
var (success, message) = TryParseNumber("123");
Console.WriteLine($"成功:{success}, 消息:{message}");

(bool, string) TryParseNumber(string input)
{
    if (int.TryParse(input, out int num))
        return (true, $"解析成功:{num}");
    else
        return (false, "解析失败");
}

注意事项:

csharp
// ❌ 错误:类型必须匹配
// (int x, string y) = ("hello", 10);  // 编译错误

// ✅ 正确:类型一一对应
(string s, int n) = ("hello", 10);

// ⚠️ 与 = 混用时的优先级
int m = 1, n2 = 2;
(m, n2) = (n2, m);  // 先计算右边 (n2, m) = (2, 1),再交错赋值

第二模块:sizeof 运算符 ​

五、sizeof 基础:内存的「尺子」 ​

sizeof 运算符用于获取类型在内存中占用的字节数。它像一把尺子,告诉你一个类型需要多大的「储物格」。

5.1 基础用法:值类型的尺寸 ​

csharp
// 基础值类型的固定尺寸(与平台无关)
Console.WriteLine(sizeof(byte));    // 输出:1
Console.WriteLine(sizeof(short));   // 输出:2
Console.WriteLine(sizeof(int));     // 输出:4
Console.WriteLine(sizeof(long));    // 输出:8
Console.WriteLine(sizeof(float));   // 输出:4
Console.WriteLine(sizeof(double));  // 输出:8
Console.WriteLine(sizeof(decimal)); // 输出:16
Console.WriteLine(sizeof(char));    // 输出:2(Unicode 字符)
Console.WriteLine(sizeof(bool));    // 输出:1

关键认知:这些值类型的尺寸是编译时常量,由 CLR 规范固定,不随平台变化。

5.2 引用类型:sizeof 的禁区 ​

csharp
// ❌ 错误:sizeof 不能用于引用类型
// Console.WriteLine(sizeof(string));     // 编译错误
// Console.WriteLine(sizeof(object));       // 编译错误
// Console.WriteLine(sizeof(int[]));        // 编译错误

// 引用类型的「尺寸」不固定:string 可能是 "a" 也可能是很长的文本
// 数组的尺寸取决于元素个数,编译时无法确定

为什么? 引用类型存储在堆上,大小不固定。sizeof 只能测量栈上固定大小的值类型。


六、unsafe 上下文:解锁自定义结构体的尺寸 ​

在安全代码(默认)中,sizeof 只能用于基础值类型。要测量自定义结构体,必须进入 unsafe 上下文。

6.1 启用 unsafe 的两种方式 ​

方式1:项目级启用(推荐用于整个项目需要 unsafe 的场景)

xml
<!-- 在 .csproj 文件中添加 -->
<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

方式2:方法级标记(推荐用于局部 unsafe 代码)

csharp
public class Program
{
    // 方法标记 unsafe
    public static unsafe void Main()
    {
        // 这里可以使用指针和 sizeof 自定义类型
    }
}

6.2 自定义结构体的 sizeof ​

csharp
// 定义一个自定义结构体
public struct Point
{
    public int X;  // 4 字节
    public int Y;  // 4 字节
    // 总计:8 字节(不考虑内存对齐)
}

public struct Color
{
    public byte R;  // 1 字节
    public byte G;  // 1 字节
    public byte B;  // 1 字节
    public byte A;  // 1 字节
    // 总计:4 字节(刚好对齐)
}

public struct Mixed
{
    public byte B;   // 1 字节
    public int I;    // 4 字节
    public short S;  // 2 字节
    // 实际占用:8 字节(1 + 3 字节填充 + 4 = 8,short 在第 8 字节...)
    // 等等,实际布局更复杂!
}

unsafe class Program
{
    static void Main()
    {
        Console.WriteLine($"Point: {sizeof(Point)} 字节");      // 输出:8
        Console.WriteLine($"Color: {sizeof(Color)} 字节");      // 输出:4
        Console.WriteLine($"Mixed: {sizeof(Mixed)} 字节");       // 输出:8(含填充!)
    }
}

内存对齐(Padding):CLR 会自动在字段之间插入「填充字节」,确保每个字段的起始地址是其大小的整数倍。这是为了 CPU 访问效率。

csharp
public struct PaddingDemo
{
    public byte A;   // 1 字节,偏移 0
    // 填充 3 字节(偏移 1,2,3)
    public int B;    // 4 字节,偏移 4(int 需要 4 字节对齐)
    // 总计:8 字节
}

// 显式控制布局:消除填充
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct NoPadding
{
    public byte A;   // 1 字节,偏移 0
    public int B;    // 4 字节,偏移 1(无填充!)
    // 总计:5 字节
}

6.3 泛型类型的 sizeof ​

csharp
// 泛型方法中使用 sizeof
public static unsafe int GetSize<T>() where T : unmanaged
{
    return sizeof(T);
}

// 调用示例
Console.WriteLine(GetSize<int>());     // 输出:4
Console.WriteLine(GetSize<double>());  // 输出:8
Console.WriteLine(GetSize<Point>());   // 输出:8(Point 是 unmanaged 类型)

// where T : unmanaged 约束:确保 T 是不含引用类型的值类型

七、不同 .NET 平台的差异 ​

特性.NET Framework.NET Core 3.1.NET 5+
基础值类型 sizeof✅ 支持✅ 支持✅ 支持
自定义结构体 sizeof需要 unsafe需要 unsafe需要 unsafe
unmanaged 约束❌ 不支持✅ C# 7.3+✅ 支持
sizeof(T) 泛型❌ 不支持✅ 支持✅ 支持
stackalloc + sizeof✅ 支持✅ 支持✅ 支持

关键变化:.NET Core 3.1 引入了 unmanaged 约束,让泛型代码也能安全使用 sizeof。.NET 5+ 进一步统一了行为。


八、sizeof vs Marshal.SizeOf:两把不同的尺子 ​

这是最容易混淆的知识点!两把尺子量的是不同的东西。

对比项sizeofMarshal.SizeOf
测量对象类型在 CLR 托管内存中的大小类型在 非托管内存中的大小
使用场景不安全代码、内存布局分析P/Invoke、COM 互操作
引用类型❌ 不支持✅ 支持(返回句柄大小)
泛型支持✅ unmanaged 约束✅ Marshal.SizeOf<T>()
性能极快(编译时常量)较慢(运行时计算)
内存对齐含 CLR 填充含平台默认对齐
csharp
using System.Runtime.InteropServices;

// 定义一个结构体
[StructLayout(LayoutKind.Sequential)]
public struct NativePoint
{
    public int X;
    public int Y;
}

unsafe class Program
{
    static void Main()
    {
        // sizeof:测量 CLR 托管内存中的大小
        Console.WriteLine($"sizeof(NativePoint): {sizeof(NativePoint)}");        // 输出:8

        // Marshal.SizeOf:测量非托管内存中的大小
        Console.WriteLine($"Marshal.SizeOf: {Marshal.SizeOf<NativePoint>()}");   // 输出:8

        // 对于简单结构体,两者通常相同

        // 但包含引用类型时,差异显现!
        // sizeof 无法用于含引用类型的结构体
        // Marshal.SizeOf 会计算非托管表示的大小
    }
}

何时用哪个?

csharp
// 场景1:分配非托管内存给 P/Invoke
IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf<NativePoint>());

// 场景2:分析值类型数组的内存占用
int elementSize = sizeof(int);
int totalSize = elementSize * 100;  // 100 个 int 的数组约 400 字节

// 场景3:与 C/C++ DLL 交互时,确保结构体布局一致
// 使用 [StructLayout] + Marshal.SizeOf 验证

九、sizeof 常见踩坑场景 ​

9.1 坑1:在安全代码中使用自定义结构体 ​

csharp
public struct MyStruct
{
    public int A;
    public int B;
}

class Program
{
    static void Main()
    {
        // ❌ 编译错误:安全上下文中 sizeof 只能用于基础类型
        // int size = sizeof(MyStruct);

        // ✅ 解决1:使用 unsafe
        unsafe
        {
            int size = sizeof(MyStruct);  // 输出:8
        }

        // ✅ 解决2:使用 Marshal.SizeOf(无需 unsafe)
        int size2 = Marshal.SizeOf<MyStruct>();  // 输出:8
    }
}

9.2 坑2:sizeof 与泛型类型参数 ​

csharp
// ❌ 错误:没有约束时,sizeof 无法用于泛型参数
public static int BadGetSize<T>()
{
    // return sizeof(T);  // 编译错误!
    return 0;
}

// ✅ 正确:添加 unmanaged 约束
public static unsafe int GoodGetSize<T>() where T : unmanaged
{
    return sizeof(T);
}

9.3 坑3:混淆 sizeof 和数组长度 ​

csharp
int[] numbers = new int[100];

// ❌ 错误:sizeof 不能用于数组类型
// int size = sizeof(int[]);  // 编译错误

// ✅ 正确:数组的内存占用 = 元素大小 × 元素个数 + 数组对象头
int elementSize = sizeof(int);           // 4
int arrayMemory = elementSize * 100 + 16;  // 约 416 字节(含对象头)

// 数组长度用 Length 属性
int length = numbers.Length;  // 100

第三模块:实战结合与综合应用 ​

十、实战案例:高性能内存缓冲区 ​

结合赋值运算符和 sizeof,构建一个固定大小的内存缓冲区。

csharp
using System.Runtime.InteropServices;

// 定义一个固定大小的数据包结构体
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DataPacket
{
    public int Id;           // 4 字节
    public long Timestamp;   // 8 字节
    public byte Status;      // 1 字节
    // 总计:13 字节(Pack=1 消除填充)
}

public unsafe class PacketBuffer
{
    private readonly byte[] _buffer;
    private readonly int _packetSize;
    private int _writePosition;

    public PacketBuffer(int capacity)
    {
        // 使用 sizeof 计算单个数据包大小
        _packetSize = sizeof(DataPacket);

        // 使用复合赋值计算总缓冲区大小
        int totalSize = 0;
        totalSize += capacity * _packetSize;  // 等价于 totalSize = totalSize + capacity * _packetSize

        _buffer = new byte[totalSize];
        _writePosition = 0;
    }

    // 写入数据包:使用交错赋值解析返回
    public (bool success, int writtenBytes) Write(DataPacket packet)
    {
        if (_writePosition + _packetSize > _buffer.Length)
        {
            return (false, 0);  // 缓冲区已满
        }

        // 使用 unsafe 代码直接操作内存
        fixed (byte* ptr = &_buffer[_writePosition])
        {
            DataPacket* packetPtr = (DataPacket*)ptr;
            *packetPtr = packet;
        }

        _writePosition += _packetSize;  // 复合赋值更新位置
        return (true, _packetSize);
    }

    // 读取数据包
    public (bool success, DataPacket packet) Read(int index)
    {
        if (index < 0 || index * _packetSize >= _writePosition)
        {
            return (false, default);
        }

        fixed (byte* ptr = &_buffer[index * _packetSize])
        {
            DataPacket* packetPtr = (DataPacket*)ptr;
            return (true, *packetPtr);
        }
    }
}

// 使用示例
class Program
{
    static unsafe void Main()
    {
        var buffer = new PacketBuffer(capacity: 100);

        // 写入数据
        var packet = new DataPacket { Id = 1, Timestamp = DateTime.Now.Ticks, Status = 1 };
        var (writeSuccess, bytesWritten) = buffer.Write(packet);
        Console.WriteLine($"写入:{writeSuccess}, 字节数:{bytesWritten}");

        // 读取数据
        var (readSuccess, readPacket) = buffer.Read(0);
        Console.WriteLine($"读取:{readSuccess}, Id={readPacket.Id}, Status={readPacket.Status}");

        // 使用 ??= 初始化配置
        string config = null;
        config ??= "默认配置";  // 如果 config 为 null,设为默认值
        Console.WriteLine($"配置:{config}");
    }
}

十一、知识点总结与最佳实践 ​

赋值运算符速查表 ​

运算符名称示例等价于
=基础赋值x = 5—
+=加赋值x += 3x = x + 3
-=减赋值x -= 3x = x - 3
*=乘赋值x *= 3x = x * 3
/=除赋值x /= 3x = x / 3
%=模赋值x %= 3x = x % 3
&=位与赋值x &= 3x = x & 3
|=位或赋值x |= 3x = x | 3
^=位异或赋值x ^= 3x = x ^ 3
<<=左移赋值x <<= 2x = x << 2
>>=右移赋值x >>= 2x = x >> 2
??=空合并赋值x ??= 3x = x ?? 3

sizeof 使用规则速查 ​

场景写法是否需要 unsafe
基础值类型sizeof(int)❌ 不需要
自定义结构体sizeof(MyStruct)✅ 需要
泛型参数sizeof(T)✅ 需要 + unmanaged 约束
引用类型sizeof(string)❌ 不允许
数组类型sizeof(int[])❌ 不允许

最佳实践清单 ​

赋值运算符:

  1. 简单赋值用 =,运算后赋值用复合运算符
  2. 空值默认值优先用 ??=,语义最清晰
  3. 交换变量用交错赋值 (a, b) = (b, a),避免临时变量
  4. 复合赋值时注意隐式转换的溢出风险

sizeof:

  1. 基础类型尺寸直接用 sizeof,无需 unsafe
  2. 自定义结构体测量用 unsafe + sizeof,或 Marshal.SizeOf(更安全)
  3. P/Invoke 场景用 Marshal.SizeOf 确保与非托管代码布局一致
  4. 不要试图用 sizeof 测量引用类型或数组——用 Length 和 Count 属性

结语 ​

赋值运算符是 C# 中最常用的「胶水」——它们把数据和变量粘合在一起。从简单的 = 到灵活的 ??=,再到一行交换两个变量的交错赋值,掌握这些工具能让你的代码更简洁、意图更明确。

sizeof 则是一把「专业尺子」——在日常业务代码中很少用到,但在需要精确控制内存、与非托管代码交互、或优化性能的关键时刻,它是不可或缺的利器。

记住:简洁不等于简单,安全不等于受限。用好这些运算符,你的 C# 代码将既优雅又高效。