写在前面:大内老A的这篇“老生常谈:值类型VS引用类型”放在微信收藏里好几个月了,终于趁着要讲JAVA传参机制的时候仔细地按照这篇博客,自己写代码跑一下,对C#的传参,ref,in,out关键字有了一个更好的理解。因此本文仅记录自己的学习心得。


1.值传递&引用传递

2.ref关键字

3.in关键字

4.out关键字

1.值传递&引用传递

C#中数据类型有两种:

  • 值类型,int, struct等,如下方的GraphStruct。
  • 引用类型,所有的class都是引用类型,如下方的Graph。

public class Graph
{
public int area { get; set; }
public int perimeter { get; set; } public Graph(int area,int perimeter)
{
this.area = area;
this.perimeter = perimeter;
}
} public struct GraphStruct
{
public int area { get; set; }
public int perimeter { get; set; }
public GraphStruct(int area, int perimeter)
{
this.area=area;
this.perimeter=perimeter;
}
}

Graph&GraphStruct

变量分配在栈中,因此变量会有一个内存地址。下方代码Utility.AsPointer<T>()方法用于获取指向该地址的指针。(如果不知道ref关键字的作用,这里就先把它理解为取地址)
var struct1 = new GraphStruct(4, 3);
var struct2 = new GraphStruct(5, 6);
var graph1 = new Graph(4, 3);
var graph2 = new Graph(5, 6); Console.WriteLine("struct1:{0}", Utility.AsPointer(ref struct1));
Console.WriteLine("struct2:{0}",Utility.AsPointer(ref struct2));
Console.WriteLine("class1:{0}", Utility.AsPointer(ref graph1));
Console.WriteLine("class2:{0}", Utility.AsPointer(ref graph2));


internal static class Utility
{
public static unsafe nint AsPointer<T>(ref T value)
{
return (nint)Unsafe.AsPointer(ref value);
}
}

上方代码的输出如下:

可以发现,地址以8字节的差值递减,即栈向下生长。下方代码用来获取变量内存上的内容,即变量的值。

var struct1 = new GraphStruct(4, 3);
var struct2 = struct1;
var graph1 = new Graph(4, 3);
var graph2 = graph1; // 输出变量内容
Console.WriteLine("struct1:{0}",BitConverter.ToString(Utility.Read(ref struct1)));
Console.WriteLine("struct2:{0}", BitConverter.ToString(Utility.Read(ref struct2)));
Console.WriteLine("class1:{0}", BitConverter.ToString(Utility.Read(ref graph1)));
Console.WriteLine("class2:{0}", BitConverter.ToString(Utility.Read(ref graph2))); internal static class Utility
{
public static unsafe nint AsPointer<T>(ref T value)
{
return (nint)Unsafe.AsPointer(ref value);
} public static unsafe byte[] Read<T>(ref T value)
{
byte[] bytes = new byte[Unsafe.SizeOf<T>()];
Marshal.Copy(AsPointer(ref value), bytes, 0, bytes.Length);
return bytes;
}
}

代码输出如下:

值类型变量内存中存的就是struct1的值04-00-00-00和03-00-00-00,两个int,刚好占了8字节。引用类型变量内存中存的是对象class1在堆内存中的地址。所以在给struct2和class2赋值的时候,其实就是把变量struct1和class1内存上的值赋了过去。传参时也是一样,虽然通常会说分为值传递和引用传递,但本质上传的都是变量内存中存的值。

下面再输出实参和形参的地址看下。

var struct1 = new GraphStruct(4, 3);
var graph1 = new Graph(4, 3); // 输出实参和形参的地址
Console.WriteLine("struct1_address:{0}", Utility.AsPointer(ref struct1));
Console.WriteLine("class1_address:{0}", Utility.AsPointer(ref graph1));
Invoke(struct1, graph1); static void Invoke(GraphStruct s, Graph c)
{
Console.WriteLine("s_args:{0}",Utility.AsPointer(ref s));
Console.WriteLine("c_args:{0}", Utility.AsPointer(ref c));
}

输出结果为:

可见在调用方法时,也会给形参变量分配栈内存。

2.ref关键字

先用结构体来看下用了ref之后,实参和形参的地址。

var struct1 = new GraphStruct(4, 3);

Console.WriteLine("struct1_address:{0}", Utility.AsPointer(ref struct1)); // 输出实参地址
modifyStruct(ref struct1); static void modifyStruct(ref GraphStruct s)
{
Console.WriteLine("args_address:{0}", Utility.AsPointer(ref s)); // 输出形参地址
}

输出结果为:

可见实参和形参在内存中的地址是相同的!那么与其说ref关键字用于传递变量自身的地址,不如把它理解为啥也没传。比如用下面的类的例子来说明下。

var graph = new Graph(5, 4);
Console.WriteLine("Original area={0}, perimeter={1}", graph.area, graph.perimeter);
modifyGraph(graph);
Console.WriteLine("After modified, area={0}, perimeter={1}",graph.area,graph.perimeter); static void modifyGraph(Graph arg_graph)
{
arg_graph = new Graph(6, 7);
}

当没有用ref关键字时,传参使得实参graph和形参arg_graph指向了同一个Graph对象,如下图所示。

在方法modifyGraph()中更改了形参的引用,即现在形参变量内存上存的是另外一个Graph对象在堆内存的地址。

在调用modifyGraph()方法前后,变量graph都指向同一个Graph对象,因此输出结果为:

下方代码在传参时,使用了ref关键字。

var graph = new Graph(5, 4);
Console.WriteLine("Original area={0}, perimeter={1}", graph.area, graph.perimeter);
modifyByReference(ref graph);
Console.WriteLine("After modified, area={0}, perimeter={1}",graph.area,graph.perimeter); static void modifyByReference(ref Graph arg_graph)
{
arg_graph = new Graph(8, 9);
}

因为啥也没传,所以变量graph就是变量arg_graph,如下图所示。引用官方文档的话就是"The ref keyword makes the formal parameter an alias for the argument."

此时在modifyByReference()方法中,令变量arg_graph指向另一个新的Graph对象,这意味着变量graph也指向了该对象。

因此上方代码输出结果为:

3.in关键字

in关键字与与ref关键字一样,都是传递变量的地址,不同的是在方法内不能改变该变量的内容。通过上面对ref的分析,可以把in关键字的作用简化为:不允许在方法内改变实参变量的值。那么对于值类型而言就意味着形参对于方法而言是一个只读变量;对于引用类型而言,可以改变对象的属性,但是不能引用其他对象。

static void modityStruct(in GraphStruct s)
{
// 下面两行代码都是错的,变量s此时是只读的
s.perimeter = 5;
s = new GraphStruct(6, 7);
}
static void modifyGraph(in Graph g)
{
g.perimeter = 7; // 可以修改属性,因为这个操作并不改变变量g所在内存中的值,即Graph对象的地址
g = new Graph(7, 8); // 不可以指向其他的Graph对象
}

4.out关键字

根据官方文档原文"The out keyword is like the ref keyword, except that ref requires that the variable be initialized before it is passed."。可见,ref关键字要求变量初始化,但out关键字没有这个要求。因此下面ref的错误,换成out就可以了。

Refs:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/method-parameters

C# ref, in, out关键字的更多相关文章

  1. 为什么C#中ref和out 关键字 ?

    需求假设:现需要通过一个叫Swap的方法交换a,b两个变量的值.交换前a=1,b=2,断言:交换后a=2,b=1. 现编码如下: class Program   {       static void ...

  2. c#中ref和out 关键字

    问题:为什么c#中要有ref和out?(而java中没有)需求假设:现需要通过一个叫Swap的方法交换a,b两个变量的值.交换前a=1,b=2,断言:交换后a=2,b=1. 现编码如下: class ...

  3. C#方法参数传递-同时使用ref和out关键字

    在方法参数传递中,可以同时使用ref和out关键字,但是要注意ref和out参数传递的不同. using System;class Program{static void Main(){    Pro ...

  4. 关于c#中”ref”和”out”关键字的一些理解

    一. 综述(本文内容大部分来自网络,经本人整理而成,仅供学习参考,不免理解错误,欢迎批评指正) 在c#中,方法的参数传递有四种类型: (1) 传值参数(by value) 传值参数无需额外的修饰符.传 ...

  5. 索引器和ref、out关键字

    这节讲三个小知识:索引器.ref.out. 索引器: 在一个类中,我们可以定义一个索引器,它可以让我们在外部像访问数组元素一样访问类的属性成员. 索引器的定义就像定义属性一样,只不过名称为this,后 ...

  6. C#中ref和out关键字的应用以及区别

    首先:两者都是按地址传递的,使用后都将改变原来参数的数值. 其次:ref可以把参数的数值传递进函数,但是out是要把参数清空,就是说你无法把一个数值从out传递进去的,out进去后,参数的数值为空,所 ...

  7. C# 中ref与out关键字区别

    ref 关键字通过引用传递的参数的内存地址,而不是值.简单点说就是在方法中对参数的任何改变都会改变调用方的基础参数中.代码举例: class RefExample { static void Meth ...

  8. C# ref与out关键字解析

    简介:ref和out是C#开发中经常使用的关键字,所以作为一个.NET开发,必须知道如何使用这两个关键字. 1.相同点 ref和out都是按地址传递,使用后都将改变原来参数的数值. 2.ref关键字 ...

  9. C#基础(204)--对象初始化器,基本数据类型与引用数据类型特点总结,ref,out关键字的使用

    对象初始化器: 对象在创建过程中也可以使用对象初始化器完成“属性的初始化” Student stu =new Student(){ StudentId=, StudentName="张三&q ...

  10. 5.C#知识点:ref和Out关键字浅谈

    首先我们要知道ref和out在C#里面是什么? 答:它们俩是C#里面的关键字. 他们俩是干啥的呢? 答:他们俩是方法参数的修饰符号,一但使用,方法定义和方法都用都要使用这个关键字,这一点是死规定. 好 ...

随机推荐

  1. JavaScript 整理的基础的方法

    innerHTML //innerHTML 属性设置或返回表格行的开始和结束标签之间的 HTML. innertext//innerText 属性返回文本值 没有结构信息 $("#test& ...

  2. JAVA生成xml文件格式

    摘要: 1.在某些业务中需要使用JAVA按照规定生成固定XML格式文件,本文中根据相应的业务生成固定的XML格式文件,并且通过测试可以发送. 2.下面代码根绝dom4j生成xml格式文件 代码思路: ...

  3. 图扑 AR 技术应用与管理:施工建造、机柜扫描、办公室导航解决方案

    随着科技的不断革新和创新,越来越多的行业开始迎来数字化时代的变革.建筑行业作为人类历史上最重要的产业之一,在数字化转型方面同样也在不断推进.图扑软件结合 AR 技术的应用,为建筑行业带来了更加便捷高效 ...

  4. iOS Block笔记总结

    前言: 对block的简单笔记总结, 1.本质: 封装了函数调用和函数调用环境的对象 2.block结构: 3.block捕获变量: 由于需要跨函数访问变量,所以需要捕获变量,(防止访问时已被销毁)  ...

  5. 【Redis】基础命令

    声明:本篇文章参考于该作者的# Redis从入门到精通:中级篇,大家有兴趣,去关注一下. 1.字符串(String) String(字符串)是Redis中最简单的一种数据结构,和MemCache数据结 ...

  6. 【环境搭建】vscode调试php

    待解决问题 使用vscode和phpstudy实现PHP的本地调试 解决办法 1.打开xdebug 找到网站使用的PHP版本,在设置中将Xdebug调试组件打开,并确认端口是9000 找到php扩展目 ...

  7. Point Free

    这是一种函数编码模式: 把数据处理的过程定义成和数据无关的合成运算,不需要用到数据参数,只是简单合成运算步骤,但需要定义一些辅助的基本运算函数. for example:  采用了lodash的fp ...

  8. Oracle11gR2单实例的安装与部署

    1   安装目标与规划 交易系统1台HP DL580 G9服务器将安装Oracle11gR2 . 1.1  安装介质版本 主机名 his1 IP地址 192.168.40.102(非生产环境ip) O ...

  9. C#性能优化-树形结构递归优化

    前言 大家好,我是wacky,最近在工作中遇到一个有趣的问题,同事反馈说WPF中有一个树形结构的集合,在加载时会直接报堆栈溢出,一直没时间(懒得)看,导致很久了也没人解决掉.于是,组长就把这个&quo ...

  10. 【动画进阶】神奇的 3D 磨砂玻璃透视效果

    最近,群友分享了一个很有意思的效果: 原效果的网址:frosted-glass.该效果的几个核心点: 毛玻璃磨砂效果 卡片的 3D 旋转跟随效果 整体透明度和磨砂感.以及卡片的 3D 形态会随着用户移 ...