目录
[1.1 是什么](#1.1 是什么)
[1.2 用来存什么](#1.2 用来存什么)
[1.3 什么时候用](#1.3 什么时候用)
[2.1 核心概念](#2.1 核心概念)
[2.2 重要区别](#2.2 重要区别)
[2.2.1 值类型](#2.2.1 值类型)
[2.2.2 引用类型](#2.2.2 引用类型)
[2.2.3 readonly struct](#2.2.3 readonly struct)
[2.2.4 readonly实例成员 (struct内的方法)](#2.2.4 readonly实例成员 (struct内的方法))
[2.2.5 ref readonly返回值](#2.2.5 ref readonly返回值)
[2.2.6 in参数(方法参数上的readonly引用)](#2.2.6 in参数(方法参数上的readonly引用))
1.const
1.1 是什么
-
它用来声明常量。常量可以是字段(类的成员)或局部变量(方法内部的变量)。
-
关键点:常量一旦设定,其值就绝对不能改变! 试图修改常量会导致编译错误。
1.2 用来存什么
-
基本类型: 数字(
int
,double
等)、布尔值(true
/false
)。 -
字符串: 固定文本(
"Hello"
)。 -
null
: 对于引用类型,只有null
可以作为常量值(除了字符串)。 -
内插字符串常量: 如果拼接的所有部分都是常量字符串,那么整个内插字符串也可以是常量
const int X = 0; // 局部常量
const string Language = "C#";
static void Main()
{
const int C = 707; // 方法内部的常量
Console.WriteLine("My local constant = {C}"); } const string FullProductName = "Language: {Language}"; //因为Language是常量字符串,所以可以
1.3 什么时候用
-
用来表示绝对不变、永恒不变的值。
-
经典例子:
-
数学常数:
const double Pi = 3.14159;
-
固定不变的枚举值(虽然通常用
enum
更好)。 -
程序中一些永远不会变的配置值(但要非常小心)。
-
-
什么时候绝对不该用
const
?-
用来表示将来可能会变的值!
-
错误例子:
-
软件版本号(会升级)
-
任何来自配置文件或数据库的值(运行时才确定)
-
-
-
为什么不能用?
- 因为
const
的值是在编译时 就确定并直接"写死"到使用它的代码里的。如果你在一个库里定义了const Version = 1;
然后另一个程序引用了这个库。当你把库里的Version
改成2
并重新编译库时,引用该库的程序必须也重新编译 ,否则它里面用的还是旧的1
!因为它编译时就把1
复制进去了。readonly
字段没有这个问题。
- 因为
2.readonly
2.1 核心概念
想象一下你有一个盒子。readonly
就像是给这个盒子贴了一个标签,规定了这个盒子什么时候可以被放进东西。
这个字段只能在两个地方被赋值:
-
声明时初始化:
public readonly int MyNumber = 10;
-
在同一个类的构造函数中:(对象构造完成,就不能再赋值)
public class MyClass { public readonly int MyNumber; public MyClass(int number) { MyNumber = number; // 允许在构造函数里赋值 } public void ChangeNumber() { // MyNumber = 20; // 错误!不能在构造函数以外的地方赋值 } }
2.2 重要区别
2.2.1 值类型
(比如 int
, struct
): 本身就直接装着数据(比如数字 10)。贴了 readonly
标签后,就不可变了。
2.2.2 引用类型
(比如 string
, List
, 自定义 class
): 本身装的是一个地址 ,指向另一个地方(堆上)的真正的对象。贴了 readonly
标签后:
-
不能把字段指向另一个对象。
public class MyClass { private readonly List<string> MyReadOnlyList = new List<string>(); // 构造时放:地址A public MyClass() { // 在构造函数里放是允许的(如果声明时没放) // MyReadOnlyList = new List<string>(); // 地址B (如果声明时没初始化) // 试图在构造函数里重新赋值 - 允许!因为还在构造阶段 // MyReadOnlyList = new List<string>(); // 现在是地址C (覆盖了之前的A或B) } public void SomeMethod() { // 试图在构造函数之外重新赋值 - 绝对不允许!编译错误! // MyReadOnlyList = new List<string>(); // 错误 CS0191:无法对只读字段赋值 } }
-
但是! 你可以根据地址找到那个对象,然后 修改对象的状态 !除非那个对象本身设计成不可变的(比如
string
)。public class MyClass { // 地址指向一个空列表 private readonly List<string> MyReadOnlyList = new List<string>(); public void AddItem(string item) { // 完全合法!我们不是换地址,我们是根据地址找到那个列表 MyReadOnlyList.Add(item); // ...然后往里添加东西! } }
补充:
警告: 如果这个对象是公共的、可变的(比如 List
),并且你通过 readonly
字段暴露了它,别人就能修改它里面的东西,这可能带来安全风险(CA2104 警告)。
public class InsecureClass
{
// 危险!公共只读字段指向可变对象
public readonly List<string> SensitiveData = new List<string>();
}
// 外部代码:
InsecureClass insecure = new InsecureClass();
insecure.SensitiveData.Add("Top Secret"); // 外部代码直接修改了内部数据!
-
核心问题:违反了面向对象编程的基本原则------封装
封装意味着一个类应该:
-
隐藏其内部状态(数据)的实现细节。
-
只通过受控的公共接口(方法、属性)来暴露和操作这些状态。
-
-
public readonly List<T> MyList
这种写法直接打破了封装:-
它完全暴露了内部数据结构: 外部代码不仅知道你有数据,还精确地知道你用一个
List<T>
来存储它。 -
它放弃了状态的控制权: 外部代码可以绕过你设计的任何业务逻辑,直接对数据进行增删改查。
-
举例:
比如你有一个购物类,里面有一个 public readonly List<CartItem> Items
。购物车添加商品时,需要检查库存、计算折扣等。但是现在外部可以随意直接修改商品,就会破坏业务规则。
正确的做法是什么?
-
首选:将字段设为
private
(或至少protected
)。private readonly List<CartItem> _items = new List<CartItem>();
-
通过属性或方法提供受控的访问: 只读视图: 返回一个只读包装器 (
IReadOnlyList<T>
,IReadOnlyCollection<T>
) 或副本。public IReadOnlyList<CartItem> Items => _items.AsReadOnly(); // 或者返回副本 (如果集合不大且频繁访问不是问题) // public List<CartItem> Items => new List<CartItem>(_items);
-
操作方法: 提供
AddItem
,RemoveItem
,ClearCart
等方法。在这些方法内部实现业务逻辑、验证、通知、线程同步等。public void AddItem(CartItem item) { // 检查库存... // 应用折扣规则... }
2.2.3 readonly struct
-
规则: 规定
struct
实例一旦建好,里面的所有东西都不能再改变。 -
它强制要求:
-
所有字段都必须是
readonly
。 -
所有方法(除了构造函数)都不能修改结构的状态(编译器会检查)。
-
-
目的:提高性能(编译器可以做更多优化)和保证数据安全。
2.2.4 readonly实例成员 (struct内的方法)
-
规则: 贴在
struct
内部的一个方法上。表示这个方法保证不会动struct
实例里的任何东西(不会修改字段)。 -
编译器会检查这个方法确实没有修改任何字段。
-
目的:告诉编译器和使用者这个方法很安全,不会改变结构状态。也可以用在属性的
get
访问器上,表示get
不会改变对象状态(即使它内部可能有计算)。public struct Point { public int X; // readonly 方法:只读取字段,不修改 public readonly void Print() { Console.WriteLine($"({X})"); // 允许:读取字段 } // readonly 方法:尝试修改字段 public readonly void Move(int deltaX) { X += deltaX; //编译错误 CS1604: 无法对只读成员赋值 } // readonly 方法:尝试修改整个实例 public readonly void Reset() { this = new Point(); //编译错误 CS1604: 无法对只读成员赋值 } // readonly 属性 get 访问器:只读访问字段 public readonly double Width => X; // 允许 // readonly 属性 get 访问器:基于字段计算 public readonly double Area { get { return X * X; } // 允许:读取字段计算 } }
补充:
-
传统属性声明(带显式
get
)public double Width
{
get { return X; } // 显式 get 访问器
} -
表达式体属性(C# 6+ 引入的简写)
public double Width => X; // 等同于上面的写法
2.2.5 ref readonly返回值
-
规则: 一个方法返回一个引用 (指向内存位置的指针),但同时加上
readonly
表示:"可以通过这个指针看 那个地方的东西,但绝对不允许 通过这个指针去修改!" -
目的:避免复制整个对象(特别是大的结构体),提高性能 ,同时保证调用者不能意外修改原始数据。
举例:
public struct LargeData
{
public int Value1;
public int Value2;
// ...假设还有很多其他字段,使得这个结构体很大
}
public class DataHolder
{
private LargeData _data = new LargeData { Value1 = 10, Value2 = 20 };
// 返回对内部数据的只读引用(不复制)
public ref readonly LargeData GetDataRef() => ref _data;
}
那为什么需要 ref呢
?
-
避免复制开销(性能优化)
// 返回副本(复制整个结构体)
public LargeData GetDataCopy() => _data;// 返回引用(不复制)
public ref readonly LargeData GetDataRef() => ref _data; -
提供直接访问原始数据的能力
public class DataHolder
{
private LargeData _data = new LargeData { Value = 42 };// 返回引用 public ref readonly LargeData GetDataRef() => ref _data;
}
// 使用
var holder = new DataHolder();
ref readonly var data = ref holder.GetDataRef();
Console.WriteLine(data.Value); // 直接访问原始数据
ref
和 ref readonly
的区别
特性 | ref |
ref readonly |
---|---|---|
能否修改数据 | ✅ 可以修改 | ❌ 不能修改 |
用途 | 需要修改原始数据时 | 需要高效读取但不修改时 |
安全性 | 可能意外修改数据 | 编译器强制保护原始数据 |
补充:在之前的文章中也提及过ref,主要是在foreach语句中的使用,感兴趣的可以点击链接去阅读2.2章节
2.2.6 in参数(方法参数上的readonly引用)
-
规则: 是
ref readonly
参数的语法糖(只读引用参数) -
目的:高效传递大型结构体(避免复制),同时保证方法内部不会修改原始数据
举例:
public struct BigStruct
{
public int Data1;
public int Data2;
// ... 其他很多字段
}
public class Processor
{
// 1. 传值 (复制整个结构体)
public void ProcessByValue(BigStruct data)
{
// 可以修改副本,不影响原始数据
data.Data1 = 100;
}
// 2. ref 引用传递 (可修改原始数据)
public void ProcessByRef(ref BigStruct data)
{
// 直接修改原始数据!
data.Data1 = 100;
}
// 3. in 只读引用传递 (重点!)
public void ProcessByIn(in BigStruct data)
{
// 读取数据 ✅
Console.WriteLine(data.Data1);
// 尝试修改 ❌ 编译错误!
// data.Data1 = 100; // 错误 CS8332: 无法对只读变量赋值
}
}
3.比较
readonly
vs const
-
const
:-
是编译时常量。
-
必须 在声明时初始化,值必须在写代码时就确定(比如
const int Max = 100;
) -
值绝对不可变 。编译后,所有用到
Max
的地方都被直接替换成100
-
只能是基本类型(
int
,string
等)或null
-
-
readonly
:-
是运行时常量 。值可以在运行时 确定(比如在构造函数里根据当前时间赋值
readonly DateTime Created = DateTime.Now;
)。 -
可以在声明时或 在类的构造函数中初始化。
-
初始化后值不可变。编译后,访问的是那个字段的内存位置。
-
可以是任何类型。
-
学到了这里,咱俩真棒,记得按时吃饭 (生活鸡飞蛋挞~)
【本篇结束,新的知识会不定时补充】
感谢你的阅读!如果内容有帮助,欢迎 点赞❤️ + 收藏⭐ + 关注 支持! 😊