首页 > 编程语言 >C# ref, in, out关键字

C# ref, in, out关键字

时间:2023-10-20 20:44:27浏览次数:41  
标签:perimeter Console C# graph WriteLine Graph ref out

写在前面:大内老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

 

标签:perimeter,Console,C#,graph,WriteLine,Graph,ref,out
From: https://www.cnblogs.com/larissa-0464/p/17775644.html

相关文章

  • C++学习笔记Day2
    关于String对象的一些事1.string对象来源于C++标准库<string>,表示一种可变长的字符序列,定义在命名空间std之中。2.string对象无初始值默认为空字符串。3.若是使用等号对string变量进行初始化,属于拷贝初始化,不使用等号,如strings6("hiya");strings7{"11123"};strings8(10,"c......
  • grep console 配色
    FATAL颜色设定"800000"ERROR颜色设定"FF0000"WARN颜色设定"FFFF00"INFO颜色设定"008000"DEBUG颜色设定"51B1A7"TRACE颜色设定"808080"......
  • Diagnostic Port on Electronic Engine and Transmission
    DiagnosticConnectorTesttheConnectiontotheECMusingcatetdiagnostickitToolkitDeutschConnectors(6/9-Pin)NOTE:OntheDeutsch9-pinSAEStandardHeavy-DutyTruckConnector,pinsHandJarelabeled"OEMSpecific".SometruckOEMshaveu......
  • C#输出文字对齐,空格位数对齐
    [C#]Console.WriteLine("-------------------------------");Console.WriteLine("FirstName|LastName|Age");Console.WriteLine("-------------------------------");Console.WriteLine($"{"Bill",-10}|{"G......
  • QT cmake工程使用QXlsx源码操作execl,无需编译QXlsx,也不需要下载其他东西,windows和ubu
    一、下载地址:链接二、进入下载好的QXlsx目录下,取出QXlsx目录和README.md待用三、用qt创建一个简单的cmake工程,将QXlsx目录和README.md文件放到cmakelists.txt所在目录 四、修改cmakelists.txt文件cmake_minimum_required(VERSION3.5)project(xlsxTestLANGUAGESCXX)......
  • 栈实现算术优先级运算c++
    #include<stdlib.h>#include<stdio.h>#include<iostream>usingnamespacestd;#defineSTACK_INIT_SIZE100//栈初始开辟空间大小#defineSTACK_INCREMENT10//栈追加空间大小//优先级数组,2表示top>c,1表示top<c,0表示top=c,-1表示错误intprior[7][7]={{2,2,......
  • MC咸蛋超人代码
    #include<iostream>#include"minecraft.h"usingnamespacestd;TxMinecraftmc;intx=-73,y=143,z=-254;intmain(intargc,char**argv){boolcon=mc.ConnectMinecraft("zk.makeblock.net.cn","a9d44e758f6e4cf8b2da2624156f24d3&q......
  • UIAUTOMATION UIA Inspect.exe UIspy.exe 使用备忘
    一、安装inspect.exe的官网地址:  WindowsSDK-Windows应用开发|MicrosoftDeveloper在VisualStudio2022安装程序的可选组件中选择“Windows11SDK(10.0.22621.0)  百度 usingSystem.Windows.Automation;  二、样例https://learn.microsoft.com/en-us......
  • 06Date类和Calendar类
    Date类Date表示特定的瞬间,精确到毫秒。Date类中的大部分方法都已经被Calendar类中的方法所取代。时间单位:1秒=1000毫秒1毫秒=1000微秒1微妙=1000纳秒案例:publicstaticvoidmain(String[]args){ //创建一个Date对象 //今天 Datedate1=newDate();/......
  • 05BigDecimal类
    BigDecimalBigDecimal是一个精度更高的一个类。位置:java.math包中。作用:精确计算浮点数。创建方式:BigDecimalbd=newBigDecimal("1.0");方法:BigDecimaladd(BigDecimalbd);加BigDecimalsubtract(BigDecimalbd);减BigDecimalmultiply(BigDecimalbd);乘......