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

news/2025/10/14 12:51:01/文章来源:https://www.cnblogs.com/slgkaifa/p/19140493

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 用来存什么

  • ​基本类型:​​ 数字(intdouble等)、布尔值(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 ​​值类型

(比如 intstruct):​​ 本身就直接装着数据(比如数字 10)。贴了 readonly标签后,​​就不可变了​​。

​​2.2.2 引用类型

(比如 stringList, 自定义 class):​​ 本身装的是一个​​地址​​,指向另一个地方(堆上)的真正的对象。贴了 readonly标签后:

  • ​不能把字段指向另一个对象。​

    public class MyClass
    {private readonly List MyReadOnlyList = new List(); // 构造时放:地址Apublic MyClass(){// 在构造函数里放是允许的(如果声明时没放)// MyReadOnlyList = new List(); // 地址B (如果声明时没初始化)// 试图在构造函数里重新赋值 - 允许!因为还在构造阶段// MyReadOnlyList = new List(); // 现在是地址C (覆盖了之前的A或B)}public void SomeMethod(){// 试图在构造函数之外重新赋值 - 绝对不允许!编译错误!// MyReadOnlyList = new List(); // 错误 CS0191:无法对只读字段赋值}
    }
  • ​但是!​​ 你可以根据地址找到那个对象,然后​修改对象的状态​​!除非那个对象本身设计成不可变的(比如 string)。

    public class MyClass
    {// 地址指向一个空列表private readonly List MyReadOnlyList = new List();public void AddItem(string item){// 完全合法!我们不是换地址,我们是根据地址找到那个列表MyReadOnlyList.Add(item); // ...然后往里添加东西!}
    }

​补充:

警告:​​ 如果这个对象是公共的、可变的(比如 List),并且你通过 readonly字段暴露了它,别人就能修改它里面的东西,这可能带来安全风险(CA2104 警告)。

public class InsecureClass
{// 危险!公共只读字段指向可变对象public readonly List SensitiveData = new List();
}
// 外部代码:
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 _items = new List();
  • ​通过属性或方法提供受控的访问:​​​只读视图:​​ 返回一个只读包装器 (IReadOnlyList<T>IReadOnlyCollection<T>) 或副本。
    public IReadOnlyList Items => _items.AsReadOnly();
    // 或者返回副本 (如果集合不大且频繁访问不是问题)
    // public List Items => new List(_items);
  • 操作方法:​​ 提供 AddItemRemoveItemClearCart等方法。在这些方法内部实现业务逻辑、验证、通知、线程同步等。
    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章节

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

    • 只能是基本类型(intstring等)或 null

  • readonly:​

    • 是​​运行时常量​​。值可以在​​运行时​​确定(比如在构造函数里根据当前时间赋值 readonly DateTime Created = DateTime.Now;)。

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

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

    • 可以是任何类型。

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/936780.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

加州新规要求AI必须表明其AI身份

加州通过SB 243法案,要求伴侣聊天机器人必须明确告知用户其AI身份,并建立自杀预防报告机制。该法案旨在保护儿童安全,要求AI开发者实施防护措施,防止用户误以为在与真人交流。加州新规要求AI必须表明其AI身份 一项…

详细介绍:【rabbitmq 高级特性】全面详解RabbitMQ TTL (Time To Live)

详细介绍:【rabbitmq 高级特性】全面详解RabbitMQ TTL (Time To Live)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: &…

第三台中转机实现远程scp文件到远程

点击查看代码 命令 scp -3r user@ip:/path/file user@ip:/path/ 远程主机(源)-》中转机-》远程主机(目标)如果要实现免密需要 ssh-copy-id user@ip 将本地的 SSH 公钥快速复制到远程主机的 ~/.ssh/authorized_key…

单片机使用同一硬件定时器实现多周期定时功能

一个复杂的单片机程序可能需要很多种周期不同的定时器,用于执行不同的任务,如传感器数据采集、显示设备刷新或者执行设备的驱动等。如果每种周期使用一个单片机的硬件定时器将很难实现全部的功能需求,本文记录一种简…

(二十六)、Kuboard 部署网络问题 k8s 使用本地镜像 k8s使用 register本地镜像站 综合应用 - 实践

(二十六)、Kuboard 部署网络问题 &k8s 使用本地镜像 & k8s使用 register本地镜像站 综合应用 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; displa…

低代码平台底层协议设计

低代码平台底层协议设计 1. 核心协议架构 1.1 协议分层设计 // 低代码平台协议栈 interface LowCodeProtocolStack {// 1. 传输层协议transport: TransportProtocol;// 2. 数据描述协议schema: SchemaProtocol;// 3. 组…

从PHP到Spring Boot:思维的转变与入门实战 (指南二) - 教程

从PHP到Spring Boot:思维的转变与入门实战 (指南二) - 教程2025-10-14 12:27 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !impor…

Vue 低代码平台渲染引擎设计

Vue 低代码平台渲染引擎设计 1. 核心架构设计 1.1 整体架构 // 渲染引擎核心接口定义 interface RenderEngine {schema: PageSchema; // 页面Schemacomponents: ComponentMap; // 组件映射dataSource: D…

微前端架构:实战指南与未来趋势 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

基于海思Hi3798MV200 Android7.0达成电影播放蓝光导航功能

基于海思Hi3798MV200 Android7.0达成电影播放蓝光导航功能pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consola…

2025 年热处理钎焊炉工装夹具厂家推荐榜:钎焊炉用耐热钢工装夹具厂家,聚焦品质与适配,助力企业高效生产

随着制造业对热处理工艺精度要求的不断提升、设备耐用性需求增强及生产标准化推进,热处理钎焊炉工装夹具已从高端冶金、核工业领域逐步拓展至石油、化工、电力、矿山等多个行业,2025 年市场规模预计持续增长。但市场…

实用指南:基于Spring Boot与SSM的社团管理系统架构设计

实用指南:基于Spring Boot与SSM的社团管理系统架构设计pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas&…

请求超时重试封装

请求超时重试封装 1. 基础版本 - 带指数退避的重试机制 interface RetryConfig {maxRetries?: number; // 最大重试次数baseDelay?: number; // 基础延迟时间(ms)timeout?: number; …

完整教程:数据结构 01 线性表

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

编程脉络梳理

编程脉络梳理编程脉络梳理 Java基础 源码和原理ThreadLocal 内存溢出问题 和 java引用类型定时任务Timer的原理和使用hashMap扩容和转红黑树条件Serializable接口 和 serialVersionUID 的关系指针压缩原理和为什么指针…

Emacs常用的一些快捷键,记不住的,方便查询!!

emacs 快捷键 基本快捷键(Basic) C-x C-f "find"文件, 即在缓冲区打开/新建一个文件 C-x C-s 保存文件 C-x C-w 使用其他文件名另存为文件 C-x C-v 关闭当前缓冲区文件并打开新文件 C-x i 在当前光标处插入文…

Microsoft Visual C++,Microsoft Visual Studio for Office Runtime,Microsoft Visual Basic Runtime等下载

Visual C++ 运行库合集(VCRedistPack),“缺少运行库”报错等问题修复 这个没什么好说的,就是解决常见的Visual C++ 运行库问题,一搬安装软件,比如PS,CAD等,也有因为安装游戏时出现的一些未知错误,“缺少运行库…

2025 年耐热钢厂家及热处理工装设备厂家推荐榜:多用炉/真空炉/台车炉/井式炉/箱式炉/耐热钢工装厂家,聚焦高效适配,助力企业精准选型

随着工业制造向高端化、精密化升级,热处理、冶金、石化等行业对耐热钢材料及专用工装设备的性能要求持续提升,兼具耐高温、耐腐蚀、高强度特性的耐热钢产品,已成为保障生产稳定性、提升工艺水平的核心要素。2025 年…

实用指南:如何进行WGBS的数据挖掘——从甲基化水平到功能通路

实用指南:如何进行WGBS的数据挖掘——从甲基化水平到功能通路pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Con…