C#知识学习-015(修饰符_4)

目录

1.const

[1.1 是什么​​](#1.1 是什么)

[​​1.2 用来存什么​](#1.2 用来存什么)

[1.​​3 什么时候用​​](#1.3 什么时候用)

2.readonly

[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引用))

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>(); // 构造时放:地址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); // 直接访问原始数据

refref readonly的区别

特性 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;)。

    • 可以在声明时​​或​ ​在类的​​构造函数​​中初始化。

    • 初始化后值​​不可变​​。编译后,访问的是那个字段的内存位置。

    • 可以是任何类型。

学到了这里,咱俩真棒,记得按时吃饭 (生活鸡飞蛋挞~)

【本篇结束,新的知识会不定时补充】

感谢你的阅读!如果内容有帮助,欢迎 ​​点赞❤️ + 收藏⭐ + 关注​​ 支持! 😊

相关推荐
无名指的等待7123 小时前
Redisson的Lock和TryLock的区别
java·开发语言·数据库
yanqiaofanhua3 小时前
C语言自学--自定义类型:结构体
c语言·开发语言·算法
Skrrapper3 小时前
【C++】STL:Stack详解
开发语言·c++
向前阿、3 小时前
数据结构从入门到实战————栈
c语言·开发语言·数据结构·程序人生
asdzx673 小时前
使用C#将Markdown转换为Word或PDF:高效文档转换的利器
经验分享·c#
sali-tec3 小时前
C# 基于halcon的视觉工作流-章39-OCR识别
开发语言·图像处理·算法·计算机视觉·c#·ocr
lightqjx3 小时前
【C++】vector 使用和实现
开发语言·c++
mudtools3 小时前
.net操作Excel:图表 (Chart) 的创建与定制
c#·.net·excel·wps
蓝桉~MLGT3 小时前
Python学习历程——基础语法(print打印、变量、运算)
开发语言·python·学习