C#知识学习-015(修饰符_4)
目录
1.const
1.1 是什么
1.2 用来存什么
1.3 什么时候用
2.readonly
2.1 核心概念
2.2 重要区别
2.2.1 值类型
2.2.2 引用类型
2.2.3 readonly struct
2.2.4 readonly实例成员 (struct内的方法)
2.2.5 ref readonly返回值
2.2.6 in参数(方法参数上的readonly引用)
3.比较
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>(); // 构造时放:地址Apublic 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,主要是在foreach语句中的使用,感兴趣的可以点击链接去阅读2.2章节
C#知识学习-005(迭代语句)https://blog.csdn.net/c20220924/article/details/149833524?ops_request_misc=%257B%2522request%255Fid%2522%253A%25225bd6f7cb5e484c290bd82e33c8b29440%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=5bd6f7cb5e484c290bd82e33c8b29440&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-149833524-null-null.nonecase&utm_term=ref&spm=1018.2226.3001.4450https://blog.csdn.net/c20220924/article/details/149833524?ops_request_misc=%257B%2522request%255Fid%2522%253A%25225bd6f7cb5e484c290bd82e33c8b29440%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=5bd6f7cb5e484c290bd82e33c8b29440&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-149833524-null-null.nonecase&utm_term=ref&spm=1018.2226.3001.4450
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;
)。 -
可以在声明时或在类的构造函数中初始化。
-
初始化后值不可变。编译后,访问的是那个字段的内存位置。
-
可以是任何类型。
-
学到了这里,咱俩真棒,记得按时吃饭(生活鸡飞蛋挞~)
【本篇结束,新的知识会不定时补充】
感谢你的阅读!如果内容有帮助,欢迎 点赞❤️ + 收藏⭐ + 关注 支持! 😊