写在前面:大内老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
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(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(ref T value) { return (nint)Unsafe.AsPointer(ref value); } public static unsafe byte[] Read(ref T value) { byte[] bytes = new byte[Unsafe.SizeOf()]; 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}",Utilit服务器托管网y.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); Conso服务器托管网le.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
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
六、Amazon DeepRace
目录 一、前言 二、什么是 Amazon DeepRacer 三、如何构建自己的第一个强化学习模型 1、创建 Amazon DeepRacer 资源 2、自定义你的赛道 3、开始你的模型 4、关于优化模型 5、在仿真器中测试 6、在真实赛道上测试你的模型 四、…