从C#中的结构体和类的区别中看引用和值的问题

在 C#中,结构体(struct)和类(class)都是用于创建自定义数据类型的方式,但它们在很多方面存在着显著的区别。掌握他们的区别至少不会产生一些我们不了解情况下发生的错误。

文章目录

    • 一、作为参数传递时的差别
      • 1. 类参数
      • 2. 结构体参数
    • 二、内存分配上的区别
      • 1. 类(引用类型)
      • 2. 结构体(值类型)
    • 三、使用场景
      • 1. 使用结构体的情况
      • 2. 使用类的情况
    • 四、对结构体实现引用的操作
      • 1. 使用`ref`关键字
      • 2. 使用`in`关键字(用于只读引用)
      • 3. 将结构体包装在类中(间接引用)
    • 五、防止类作为参数被修改的操作
      • 1. 使用接口和不可变类型(推荐)
      • 2 .使用`in`关键字(C# 7.2及以上)
      • 3. 传递副本(如果适用)
      • 总结

一、作为参数传递时的差别

1. 类参数

  • 当类的实例对象作为参数传递给一个方法时,传递的是对象的引用。这意味着在方法内部对参数对象的修改会影响到原始对象。
  • 示例代码:
class MyClass
{public int Value { get; set; }
}class Program
{static void ModifyClassObject(MyClass obj){obj.Value = 20;}static void Main(){MyClass classInstance = new MyClass();classInstance.Value = 10;ModifyClassObject(classInstance);Console.WriteLine($"Class - Value after modification: {classInstance.Value}");// 输出为20,因为在ModifyClassObject方法中修改了对象的属性,而传递的是引用}
}

2. 结构体参数

  • 当结构体的实例作为参数传递给一个方法时,传递的是结构体的副本。这意味着在方法内部对参数结构体的修改不会影响到原始结构体。
  • 示例代码:
struct MyStruct
{public int Value;
}class Program
{static void ModifyStructObject(MyStruct structObj){structObj.Value = 20;}static void Main(){MyStruct structInstance = new MyStruct();structInstance.Value = 10;ModifyStructObject(structInstance);Console.WriteLine($"Struct - Value after modification: {structInstance.Value}");// 输出为10,因为在ModifyStructObject方法中修改的是副本,原始结构体没有被修改}
}

二、内存分配上的区别

1. 类(引用类型)

  • 类的对象实例存储在堆(Heap)内存中。堆是一个用于动态分配内存的区域,由垃圾回收器(Garbage Collector)管理。当创建一个类的对象时,会在堆中分配足够的内存来存储对象的所有成员(字段、属性等)。变量(引用)本身存储在栈(Stack)中,它包含了对象在堆中的内存地址,通过这个引用可以访问堆中的对象。
  • 示例代码:
class MyClass
{public int Value;
}class Program
{static void Main(){MyClass obj = new MyClass();// 这里,obj是一个引用,存储在栈中,它指向堆中实际的MyClass对象// 堆中的MyClass对象存储了Value字段}
}

2. 结构体(值类型)

  • 结构体的实例通常存储在栈(Stack)中。栈是一种后进先出(LIFO)的数据结构,用于存储局部变量和方法调用的信息等。当定义一个结构体变量时,如果它是一个局部变量或者作为另一个值类型的成员,它会在栈中分配内存来存储其成员。然而,如果结构体作为一个引用类型的成员(如类的字段),那么结构体实例会跟随引用类型一起存储在堆中。
  • 示例代码:
struct MyStruct
{public int Value;
}class Program
{static void Main(){MyStruct structObj;// structObj是一个结构体,存储在栈中,直接包含Value成员}
}

三、使用场景

1. 使用结构体的情况

  • 数据简单且占用空间小:当需要表示简单的数据集合,如点坐标(xy)、矩形的尺寸(widthheight)等,结构体是一个很好的选择。因为结构体在内存中布局紧凑,对于小型数据结构可以减少内存开销。
  • 按值传递语义需求:如果希望数据在传递过程中(如作为参数传递给方法或者赋值给其他变量)是按值复制的,以确保数据的独立性,那么结构体符合要求。例如,在一些数学计算或者图形处理的函数中,不希望函数内部对参数的修改影响到原始数据。
  • 性能敏感且无继承需求:在对性能要求较高的场景中,特别是涉及大量数据的复制和操作,结构体的性能可能更优(因为栈内存的访问速度通常比堆内存快)。并且如果不需要继承和多态的功能,结构体可以满足需求。

2. 使用类的情况

  • 需要引用语义:当希望多个变量引用同一个对象实例,并且通过任何一个引用修改对象都会影响到其他引用所指向的对象时,应该使用类。例如,在对象之间共享状态或者实现观察者模式等场景。
  • 复杂的业务逻辑和行为:如果数据类型需要包含复杂的方法、属性访问逻辑、事件等,类提供了更好的组织和封装方式。通过将数据和操作封装在类中,可以更好地实现面向对象的设计原则。
  • 继承和多态需求:如果需要构建一个类型层次结构,通过继承来共享公共的属性和行为,并且利用多态来实现不同子类的特定行为,那么必须使用类。例如,在图形绘制系统中,有Shape基类,以及CircleRectangle等派生类,可以通过继承和多态来统一处理不同形状的绘制操作。

四、对结构体实现引用的操作

1. 使用ref关键字

  • 在 C#中,可以使用ref关键字来实现将结构体作为引用传递。这样,在方法内部对结构体的修改就会影响到原始的结构体。
  • 示例代码:
struct Point
{public int X;public int Y;
}
class Program
{static void ModifyPoint(ref Point p){p.X = 100;p.Y = 100;}static void Main(){Point originalPoint = new Point();originalPoint.X = 0;originalPoint.Y = 0;ModifyPoint(ref originalPoint);Console.WriteLine($"Original Point: X = {originalPoint.X}, Y = {originalPoint.Y}");// 输出为Original Point: X = 100, Y = 100,因为使用了ref关键字,修改影响了原始结构体}
}

2. 使用in关键字(用于只读引用)

  • in关键字也可以用于传递结构体的引用,但它表示传递的是一个只读引用。这意味着在方法内部不能修改结构体的成员。这种方式在需要将结构体传递给方法进行访问,但又不希望方法修改结构体的情况下很有用。
  • 示例代码:
struct Point
{public int X;public int Y;
}
class Program
{static void PrintPoint(in Point p){Console.WriteLine($"Point: X = {p.X}, Y = {p.Y}");// p.X = 100;  // 这行代码会报错,因为in关键字表示只读引用}static void Main(){Point originalPoint = new Point();originalPoint.X = 0;originalPoint.Y = 0;PrintPoint(in originalPoint);}
}

3. 将结构体包装在类中(间接引用)

  • 另一种实现对结构体引用的方式是将结构体作为类的一个成员字段,然后通过类的引用来操作结构体。
  • 示例代码:
struct Point
{public int X;public int Y;
}
class PointContainer
{public Point ThePoint;
}
class Program
{static void ModifyPointInContainer(PointContainer container){container.ThePoint.X = 100;container.ThePoint.Y = 100;}static void Main(){PointContainer container = new PointContainer();container.ThePoint = new Point();container.ThePoint.X = 0;container.ThePoint.Y = 0;ModifyPointInContainer(container);Console.WriteLine($"Point in container: X = {container.ThePoint.X}, Y = {container.ThePoint.Y}");// 输出为Point in container: X = 100, Y = 100,通过类引用修改了结构体成员}
}

五、防止类作为参数被修改的操作

1. 使用接口和不可变类型(推荐)

  • 定义接口
    • 首先,定义一个接口来描述类的行为。接口只包含方法签名,没有具体的实现,这样可以限制对类的访问和操作。
    • 例如,假设有一个Person类,定义一个IPersonReadOnly接口来提供只读方法:
interface IPersonReadOnly
{string GetName();int GetAge();
}
class Person : IPersonReadOnly
{private string name;private int age;public Person(string name, int age){this.name = name;this.age = age;}public string GetName(){return name;}public int GetAge(){return age;}
}
  • 传递接口作为参数
    • 当传递Person类的实例作为参数时,将其作为IPersonReadOnly接口类型传递。这样,接收参数的方法只能调用接口中定义的只读方法,无法修改类的内部状态。
    • 示例代码如下:
class Program
{static void PrintPersonInfo(IPersonReadOnly person){Console.WriteLine($"Name: {person.GetName()}, Age: {person.GetAge()}");}static void Main(){Person person = new Person("John", 30);PrintPersonInfo(person);}
}
  • 这种方式的优点是通过接口的抽象,强制实现了只读访问,符合面向对象设计中的依赖倒置原则,使得代码更加灵活和可维护。同时,Person类本身可以是不可变的(如果不提供修改属性的方法),进一步确保了数据的稳定性。

2 .使用in关键字(C# 7.2及以上)

  • in关键字用于传递参数的只读引用。它类似于ref关键字,但不允许在方法内部修改引用的对象。
  • 例如,对于Person类:
class Person
{public string Name { get; }public int Age { get; }public Person(string name, int age){Name = name;Age = age;}
}
class Program
{static void PrintPersonInfo(in Person person){Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");// person.Name = "New Name";  // 这行代码会报错,因为in关键字表示只读引用}static void Main(){Person person = new Person("Alice", 25);PrintPersonInfo(person);}
}
  • 使用in关键字可以明确表示参数是只读的,编译器会防止在方法内部意外地修改对象。不过需要注意的是,in关键字传递的仍然是引用,只是禁止了修改操作。如果对象是可变的并且在方法调用之外被修改,这些变化在方法内部是可见的(因为它是引用类型)。

3. 传递副本(如果适用)

  • 如果类实现了ICloneable接口或者有合适的复制构造函数,可以考虑传递类的副本而不是原始引用。
  • 例如,假设Person类实现了ICloneable接口:
class Person : ICloneable
{public string Name { get; set; }public int Age { get; set; }public object Clone(){return new Person(Name, Age);}
}
class Program
{static void PrintPersonInfo(Person person){Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");// 在这里修改person不会影响原始对象person.Name = "New Name";}static void Main(){Person person = new Person("Bob", 35);PrintPersonInfo((Person)person.Clone());Console.WriteLine($"Original Name: {person.Name}");}
}

这种方法的缺点是复制对象可能会带来性能开销,特别是对于复杂的对象或者对象图。并且如果对象的复制逻辑比较复杂(例如,对象包含引用其他对象的字段,需要进行深度复制),实现起来可能会比较困难。因此,这种方法通常在对象比较简单或者对性能要求不高的情况下使用。

总结

在 C#中,结构体和类有着多方面的区别且各有其适用场景。

在作为参数传递时,类传递的是引用,方法内对参数的修改会影响原始对象;而结构体传递的是副本,方法内的修改不会作用于原始结构体。在内存分配方面,类对象实例存于堆内存,通过栈中的引用访问,结构体实例通常在栈中(特殊情况除外)。

使用场景上,结构体适合表示简单、小型且无需继承和多态的数据集合,按值传递保证数据独立以及对性能要求较高的情况;类则用于需要引用语义、有着复杂业务逻辑和行为以及构建继承、多态的类型层次结构的场景。

对于结构体实现引用,可以借助 ref 关键字实现可修改的引用传递,用 in 关键字实现只读引用传递,或者将结构体包装在类中进行间接引用操作。而针对不想类在作为参数传递时被修改的情况,可通过定义接口传递接口类型实现只读访问、利用 in 关键字禁止方法内修改,或者传递副本(前提是类支持合适的复制机制)来达成目的。

理解结构体和类的这些区别以及对应的操作方法,有助于开发者根据项目实际需求合理选择使用结构体或类,编写出更高效、更符合设计要求的 C#代码,从而优化程序的性能、可维护性以及逻辑的正确性。

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

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

相关文章

Spann3R:基于DUSt3R的密集捕获数据增量式重建方法

来自作者Hengyi Wang在b站3D视觉工坊中对于该论文透彻的讲解,这里是相关重要部分的截屏。这篇博客的用途主要是自己做记录,其次分享给感兴趣的同学,最后谢谢作者大佬的认真讲解。 作者是按照这样的次序来介绍的: 首先从传统的三…

Python4-分支与循环

记录python学习,直到学会基本的爬虫,使用python搭建接口自动化测试就算学会了,在进阶webui自动化,app自动化 python基础3-分支与循环语句 python中 有哪些基本值是被当作true或者false的呢?if语句示例被视为 False 的情况被视为…

SAP-ABAP开发学习-面向对象OOALV(1)

本文目录 一、概述 面向对象开发特点 二、类与对象 程序中类的创建 Class构成要素 对象 方法 一、概述 随着SAP R/3 4.0版本的开发,ABAP语言开始引入了面向对象的开发概念。这在ABAP语言的发展过程中,面向对象(Object-oriented&#…

【实用技能】如何在 .NET C# 中的邮件合并过程中操作表格单元格

TX Text Control 中的邮件合并 类是一个强大的库,旨在通过将数据合并到模板中来自动创建文档。它充当结构化数据(例如来自数据库、JSON 或 XML)和动态文档生成之间的桥梁,对于需要自动化文档工作流程的应用程序来说非常有用。 TX…

有源模拟滤波器的快速设计

本文章是笔者整理的备忘笔记。希望在帮助自己温习避免遗忘的同时,也能帮助其他需要参考的朋友。如有谬误,欢迎大家进行指正。 一、概述 几乎所有电子电路中都能看到有源模拟滤波器的身影。音频系统使用滤波器进行频带限制和平衡。通信系统设计使用滤波…

如何使用Python库连接Redis

1、redis-py 库封装一个 Redis 工具类可以帮助我们简化 Redis 的操作并提高代码的复用性和可维护性。 安装redis pip install redisimport redis import logginglogging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__)class RedisUtils:def __init__(s…

【代码随想录day57】【C++复健】 53. 寻宝(prim算法);53. 寻宝(kruskal算法)

53. 寻宝(prim算法) 好像在研究生的算法课上学过prim算法和kruskal算法,不过当时只是了解了一下大致的概念和流程,并没有涉及到如何去写代码的部分,今天也算是学习了一下这两个算法的代码应该如何去实现,还…

使用OpenTK展示3D点云图像(C#)

最近在研究3D显示,找到一款在winform上展示3D点云的控件,并且实现了点线面的展示,及光照渲染纹理贴图等功能,如下面几张图所展示。 一些基础知识可以在LearnOpenTK - OpenTK 这个网站上学习到。 我这边使用的是openTK3.3.3版本&a…

MetaGPT源码 (Memory 类)

目录 MetaGPT源码:Memory 类例子 MetaGPT源码:Memory 类 这段代码定义了一个名为 Memory 的类,用于存储和管理消息(Message)对象。Memory 提供了多种操作消息的功能,包括添加单条或批量消息、按角色或内容筛选消息、删除最新消息…

pythonOpenCV篇:0基础带你python入门之常用函数

① 二值化函数 功能:将图像转换为二值图像(黑白图像),将像素值分为两种类别:前景(白)和背景(黑)。函数:cv2.threshold()参数: src:输…

小发现,如何高级的顺序输出,逆序输出整数的每一位(栈,队列)

当我还是初学者的时候,我经常思考有没有比慢慢求每一位数字然后考虑正序,逆序输出要快的办法...长期琢磨,必有所获! 我刚学数据结构的时候还没意识到栈,队列还能这样用,虽然说有点杀鸡用牛刀的感觉&#x…

详细解析RNNoise:基于深度学习的语音噪声抑制技术

引言 在语音通信、语音识别以及音频处理领域,噪声抑制是一个至关重要的任务。环境噪声,如风声、交通声和人群声,通常会影响语音的清晰度和质量,特别是在远程通信和在线会议中。为了提高语音质量,许多噪声抑制技术应运…

【笔记】架构上篇Day6 法则四:为什么要顺应技术的生命周期?

法则四:为什么要顺应技术的生命周期? 简介:包含模块一 架构师的六大生存法则-法则四:为什么要顺应技术的生命周期?&法则四:架构设计中怎么判断和利用技术趋势? 2024-08-29 17:30:07 你好&am…

跟李笑来学美式俚语(Most Common American Idioms): Part 66

Most Common American Idioms: Part 66 前言 本文是学习李笑来的Most Common American Idioms这本书的学习笔记,自用。 Github仓库链接:https://github.com/xiaolai/most-common-american-idioms 使用方法: 直接下载下来(或者clone到本地…

【Sentinel Go】新手指南、流量控制、熔断降级和并发隔离控制

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开…

代码随想录第43天

300.最长递增子序列 # Dynamic programming. class Solution:def lengthOfLIS(self, nums: List[int]) -> int:if not nums: return 0dp [1] * len(nums)for i in range(len(nums)):for j in range(i):if nums[j] < nums[i]: # 如果要求非严格递增&#xff0c;将此行 …

Anaconda Conda Pip 的区别与联系

在Python生态中,Anaconda、Conda和Pip是三个非常重要的工具,它们在包管理和环境管理方面发挥着关键作用。 Anaconda Anaconda是一个为科学计算而设计的Python发行版,它集成了Conda、Python以及大量的数据科学相关库,如NumPy、Pandas等。Anaconda的主要优势在于它提供了一个…

Y3编辑器官方文档1:编辑器简介及菜单栏详解(文件、编辑、窗口、细节、调试)

文章目录 一、新建项目二、 编辑器主界面2.1 游戏场景2.2 导航栏/菜单栏2.3 功能栏三、菜单栏详细介绍3.1 文件3.1.1 版本管理3.1.2 项目管理(多关卡)3.1.2.1 多关卡功能说明3.1.2.2 关卡切换与关卡存档3.2 编辑3.2.1 通用设置3.2.2 键位设置3.3 窗口(日志)3.4 细节3.4.1 语言…

OpenCV相机标定与3D重建(16)将点从齐次坐标转换为非齐次坐标函数convertPointsFromHomogeneous()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::convertPointsFromHomogeneous 是 OpenCV 库中的一个函数&#xff0c;用于将点从齐次坐标&#xff08;homogeneous coordinates&#xff09;…

前端视角下的Go语法学习:创建 Go 项目

今日话题 使用 GoLand 创建 Go 项目 作者&#xff1a; 时间&#xff1a;2024年6月20日 17时16分14秒 主线任务 一、GoLand 创建项目 1、点击 “new Project” 按钮 2、已经有下载过两个 Golang SDK 版本&#xff0c;选择版本创建即可~ 3、如果没有下载过Golang SDK&#…