老生常谈:值类型 V.S. 引用类型》中花了很大的篇幅介绍ref参数针对值类型和引用类型变量的传递。在C#中,除了方法的ref参数,我们还有很多使用ref关键字传递引用/地址的场景,本篇文章作一个简单的总结。

一、参数

二、数组索引

三、方法

四、ref 结构体

五、ref 结构体字段

一、参数

如果在方法的参数(不论是值类型和引用类型)添加了ref关键字,意味着将变量的地址作为参数传递到方法中。目标方法利用ref参数不仅可以直接操作原始的变量,还能直接替换整个变量的值。如下的代码片段定义了一个基于结构体的Record类型Foobar,并定义了Update和Replace方法,它们具有的唯一参数类型为Foobar,并且前置了ref关键字。

  1. static void Update(ref Foobar foobar)
  2. {
  3. foobar.Foo = 0;
  4. }
  5.  
  6. static void Replace(ref Foobar foobar)
  7. {
  8. foobar = new Foobar(0, 0);
  9. }
  10.  
  11. public record struct Foobar(int Foo, int Bar);

基于ref参数针对原始变量的修改和替换体现在如下所示的演示代码中。

  1. var foobar = new Foobar(1, 2);
  2. Update(ref foobar);
  3. Debug.Assert(foobar.Foo == 0);
  4. Debug.Assert(foobar.Bar == 2);
  5.  
  6. Replace(ref foobar);
  7. Debug.Assert(foobar.Foo == 0);
  8. Debug.Assert(foobar.Bar == 0);

C#中的ref + Type(ref Foobar)在IL中会转换成一种特殊的引用类型Type&。如下所示的是上述两个方法针对IL的声明,可以看出它们的参数类型均为Foobar&。

  1. .method assembly hidebysig static
  2. void '<<Main>$>g__Update|0_0' (
  3. valuetype Foobar& foobar
  4. ) cil managed
  5.  
  6. .method assembly hidebysig static
  7. void '<<Main>$>g__Replace|0_1' (
  8. valuetype Foobar& foobar
  9. ) cil managed

二、数组索引

我们知道数组映射一段连续的内存空间,具有相同字节长度的元素“平铺”在这段内存上。我们可以利用索引提取数组的某个元素,如果索引操作符前置了ref关键值,那么返回的就是索引自身的引用/地址。与ref参数类似,我们利用ref array[index]不仅可以修改索引指向的数组元素,还可以直接将该数组元素替换掉。

  1. var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) };
  2.  
  3. Update(ref array[1]);
  4. Debug.Assert(array[1].Foo == 0);
  5. Debug.Assert(array[1].Bar == 2);
  6.  
  7. Replace(ref array[1]);
  8. Debug.Assert(array[1].Foo == 0);
  9. Debug.Assert(array[1].Bar == 0);

由于ref关键字在IL中被被转换成“引用类型”,所以对应的“值”也只能存储在对应引用类型的变量上,引用变量同样通过ref关键字来声明。下面的代码演示了两种不同的变量赋值,前者将Foobar数组的第一个元素的“值”赋给变量foobar(类型为Foobar),后者则将第一个元素在数组中的地址赋值给变量foobarRef(类型为Foobar&)。

  1. var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) };
  2. Foobar foobar = array[0];
  3. ref Foobar foobarRef = ref array[0];
  4.  
  5. 或者
  6.  
  7. var foobar = array[0];
  8. ref var foobarRef = ref array[0];

上边这段C#代码将会转换成如下这段IL代码。我们不仅可以看出foobar和foobarRef声明的类型的不同(Foobar和Foobar&),还可以看到array[0]和ref array[0]使用的IL指令的差异,前者使用的是ldelem(Load Element)后者使用的是ldelema(Load Element Addess)。

  1. .method private hidebysig static
  2. void '<Main>$' (
  3. string[] args
  4. ) cil managed
  5. {
  6. // Method begins at RVA 0x209c
  7. // Header size: 12
  8. // Code size: 68 (0x44)
  9. .maxstack 5
  10. .entrypoint
  11. .locals init (
  12. [0] valuetype Foobar[] 'array',
  13. [1] valuetype Foobar foobar,
  14. [2] valuetype Foobar& foobarRef
  15. )
  16.  
  17. // {
  18. IL_0000: ldc.i4.3
  19. // (no C# code)
  20. IL_0001: newarr Foobar
  21. IL_0006: dup
  22. IL_0007: ldc.i4.0
  23. // Foobar[] array = new Foobar[3]
  24. // {
  25. // new Foobar(1, 1),
  26. // new Foobar(2, 2),
  27. // new Foobar(3, 3)
  28. // };
  29. IL_0008: ldc.i4.1
  30. IL_0009: ldc.i4.1
  31. IL_000a: newobj instance void Foobar::.ctor(int32, int32)
  32. IL_000f: stelem Foobar
  33. IL_0014: dup
  34. IL_0015: ldc.i4.1
  35. IL_0016: ldc.i4.2
  36. IL_0017: ldc.i4.2
  37. IL_0018: newobj instance void Foobar::.ctor(int32, int32)
  38. IL_001d: stelem Foobar
  39. IL_0022: dup
  40. IL_0023: ldc.i4.2
  41. IL_0024: ldc.i4.3
  42. IL_0025: ldc.i4.3
  43. IL_0026: newobj instance void Foobar::.ctor(int32, int32)
  44. IL_002b: stelem Foobar
  45. IL_0030: stloc.0
  46. // Foobar foobar = array[0];
  47. IL_0031: ldloc.0
  48. IL_0032: ldc.i4.0
  49. IL_0033: ldelem Foobar
  50. IL_0038: stloc.1
  51. // ref Foobar reference = ref array[0];
  52. IL_0039: ldloc.0
  53. IL_003a: ldc.i4.0
  54. IL_003b: ldelema Foobar
  55. IL_0040: stloc.2
  56. // (no C# code)
  57. IL_0041: nop
  58. // }
  59. IL_0042: nop
  60. IL_0043: ret
  61. } // end of method Program::'<Main>$'

三、方法

方法可以通过前置的ref关键字返回引用/地址,比如变量或者数组元素的引用/地址。如下面的代码片段所示,方法ElementAt返回指定Foobar数组中指定索引的地址。由于该方法返回的是数组元素的地址,所以我们利用返回值直接修改对应数组元素(调用Update方法),也可以直接将整个元素替换掉(调用Replace方法)。如果我们查看ElementAt基于IL的声明,同样会发现它的返回值为Foobar&

  1. var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) };
  2.  
  3. var copy = ElementAt(array, 1);
  4. Update(ref copy);
  5. Debug.Assert(array[1].Foo == 2);
  6. Debug.Assert(array[1].Bar == 2);
  7. Replace(ref copy);
  8. Debug.Assert(array[1].Foo == 2);
  9. Debug.Assert(array[1].Bar == 2);
  10.  
  11. ref var self = ref ElementAt(array, 1);
  12. Update(ref self);
  13. Debug.Assert(array[1].Foo == 0);
  14. Debug.Assert(array[1].Bar == 2);
  15. Replace(ref self);
  16. Debug.Assert(array[1].Foo == 0);
  17. Debug.Assert(array[1].Bar == 0);
  18.  
  19. static ref Foobar ElementAt(Foobar[] array, int index) => ref array[index];

四、ref 结构体

如果在定义结构体时添加了前置的ref关键字,那么它就转变成一个ref结构体。ref结构体和常规结构最根本的区别是它不能被分配到堆上,并且总是以引用的方式使用它,永远不会出现“拷贝”的情况,最重要的ref 结构体莫过于Span<T>了。如下这个Foobar结构体就是一个包含两个数据成员的ref结构体。

  1. public ref struct Foobar{
  2. public int Foo { get; }
  3. public int Bar { get; }
  4. public Foobar(int foo, int bar)
  5. {
  6. Foo = foo;
  7. Bar = bar;
  8. }
  9. }

ref结构体具有很多的使用约束。对于这些约束,很多人不是很理解,其实我们只需要知道这些约束最终都是为了确保:ref结构体只能存在于当前线程堆栈,而不能转移到堆上。基于这个原则,我们来具体来看看ref结构究竟有哪些使用上的限制。

1. 不能作为泛型参数

除非我们能够显式将泛型参数约束为ref结构体,对应的方法严格按照ref结构的标准来操作对应的参数或者变量,我们才能够能够将ref结构体作为泛型参数。否则对于泛型结构体,涉及的方法肯定会将其当成一个常规结构体看待,若将ref结构体指定为泛型参数类型自然是有问题。但是针对ref结构体的泛型约束目前还没有,所以我们就不能将ref结构体作为泛型参数,所以按照如下的方式创建一个Wrapper<Foobar>(Foobar为上面定义的ref结构体,下面不再单独说明)的代码是不能编译的。

  1. // Error CS0306 The type 'Foobar' may not be used as a type argument
  2. var wrapper = new Wrapper<Foobar>(new Foobar(1, 2));
  3.  
  4. public class Wrapper<T>
  5. {
  6. public Wrapper(T value) => Value = value;
  7. public T Value { get; }
  8. }

2. 不能作为数组元素类型

数组是分配在堆上的,我们自然不能将ref结构体作为数组的元素类型,所以如下的代码也会遇到编译错误。

  1. //Error CS0611 Array elements cannot be of type 'Foobar'
  2. var array = new Foobar[16];

3. 不能作为类型和非ref结构体数据成员

由于类的实例分配在堆上,常规结构体也并没有纯栈分配的约束,ref结构体自然不能作为它们的数据成员,所以如下所示的类和结构体的定义都是不合法的。

  1. public class Foobarbaz
  2. {
  3. //Error CS8345 Field or auto-implemented property cannot be of type 'Foobar' unless it is an instance member of a ref struct.
  4. public Foobar Foobar { get; }
  5. public int Baz { get; }
  6. public Foobarbaz(Foobar foobar, int baz)
  7. {
  8. Foobar = foobar;
  9. Baz = baz;
  10. }
  11. }

或者

  1. public structure Foobarbaz
  2. {
  3. //Error CS8345 Field or auto-implemented property cannot be of type 'Foobar' unless it is an instance member of a ref struct.
  4. public Foobar Foobar { get; }
  5. public int Baz { get; }
  6. public Foobarbaz(Foobar foobar, int baz)
  7. {
  8. Foobar = foobar;
  9. Baz = baz;
  10. }
  11. }

4. 不能实现接口

当我们以接口的方式使用某个结构体时会导致装箱,并最终导致对分配,所以ref结构体不能实现任意接口。

  1. //Error CS8343 'Foobar': ref structs cannot implement interfaces
  2. public ref struct Foobar : IEquatable<Foobar>
  3. {
  4. public int Foo { get; }
  5. public int Bar { get; }
  6. public Foobar(int foo, int bar)
  7. {
  8. Foo = foo;
  9. Bar = bar;
  10. }
  11.  
  12. public bool Equals(Foobar other) => Foo == other.Foo && Bar == other.Bar;
  13. }

5. 不能导致装箱

所有类型都默认派生自object,所有值类型派生自ValueType类型,但是这两个类型都是引用类型(ValueType自身是引用类型),所以将ref结构体转换成object或者ValueType类型会导致装箱,是无法通过编译的。

  1. //Error CS0029 Cannot implicitly convert type 'Foobar' to 'object'
  2. Object obj = new Foobar(1, 2);
  3.  
  4. //Error CS0029 Cannot implicitly convert type 'Foobar' to 'System.ValueType'
  5. ValueType value = new Foobar(1, 2);

6. 不能在委托中(或者Lambda表达式)使用

ref结构体的变量总是引用存储结构体的栈地址,所以它们只有在创建该ref结构体的方法中才有意义。一旦方法返回,堆栈帧被回收,它们自然就“消失”了。委托被认为是一个待执行的操作,我们无法约束它们必须在某方法中执行,所以委托执行的操作中不能引用ref结构体。从另一个角度来讲,一旦委托中涉及针对现有变量的引用,必然会导致“闭包”的创建,也就是会创建一个类型来对引用的变量进行封装,这自然也就违背了“不能将ref结构体作为类成员”的约束。这个约束同样应用到Lambda表达式和本地方法上。

  1. public class Program
  2. {
  3. static void Main()
  4. {
  5. var foobar = new Foobar(1, 2);
  6. //Error CS8175 Cannot use ref local 'foobar' inside an anonymous method, lambda expression, or query expression
  7. Action action1 = () => Console.WriteLine(foobar);
  8.  
  9. //Error CS8175 Cannot use ref local 'foobar' inside an anonymous method, lambda expression, or query expression
  10. void Print() => Console.WriteLine(foobar);
  11. }
  12. }

7. 不能在async/await异步方法中

这个约束与上一个约束类似。一般来说,一个异步方法执行过程中遇到await语句就会字节返回,后续针对操作具有针对ref结构体引用,自然是不合法的。从另一方面来讲,async/await最终会转换成基于状态机的类型,依然会出现利用自动生成的类型封装引用变量的情况,同样违背了“不能将ref结构体作为类成员”的约束。

  1. async Task InvokeAsync()
  2. {
  3. await Task.Yield();
  4. //Error CS4012 Parameters or locals of type 'Foobar' cannot be declared in async methods or async lambda
  5. var foobar = new Foobar(1, 2);
  6. }

值得一提的是,对于返回类型为Task的异步方法,如果没有使用async关键字,由于它就是一个普通的方法,编译器并不会执行基于状态机的代码生成,所以可以自由地使用ref结构体。

  1. public Task InvokeAsync()
  2. {
  3. var foobar = new Foobar(1, 2);
  4. ...
  5. return Task.CompletedTask;
  6. }

8. 不能在迭代器中使用

如果在一个返回IEnumerable<T>的方法中使用了yield return语句作为集合元素迭代器(interator),意味着涉及的操作执行会“延迟”到作为返回对象的集合被真正迭代(比如执行foreach语句)的时候,这个时候原始方法的堆栈帧已经被回收。

  1. IEnumerable<(int Foo, int Bar)> Deconstruct(Foobar foobar1, Foobar foobar2)
  2. {
  3. //Error CS4013 Instance of type 'Foobar' cannot be used inside a nested function, query expression, iterator block or async method
  4. yield return (foobar1.Foo, foobar1.Bar);
  5. //Error CS4013 Instance of type 'Foobar' cannot be used inside a nested function, query expression, iterator block or async method
  6. yield return (foobar2.Foo, foobar2.Bar);
  7. }

9. readonly ref 结构体

顺表补充一下,我们可以按照如下的方式添加前置的readonly关键字定义一个只读的ref结构体。对于这样的结构体,其数据成员只能在被构造或者被初始化的时候进行指定,所以只能定义成如下的形式。

  1. public readonly ref struct Foobar{
  2. public int Foo { get; }
  3. public int Bar { get; }
  4. public Foobar(int foo, int bar)
  5. {
  6. Foo = foo;
  7. Bar = bar;
  8. }
  9. }
  1. public readonly ref struct Foobar
  2. {
  3. public int Foo { get; init; }
  4. public int Bar { get; init; }
  5. }
  1. public readonly ref struct Foobar
  2. {
  3. public readonly int Foo;
  4. public readonly int Bar;
  5. public Foobar(int foo, int bar)
  6. {
  7. Foo = foo;
  8. Bar = bar;
  9. }
  10. }

如果为属性定义了set方法,或者其字段没有设置成“只读”,这样的readonly ref 结构体均是不合法的。

  1. public readonly ref struct Foobar
  2. {
  3. //Error CS8341 Auto-implemented instance properties in readonly structs must be readonly.
  4. public int Foo { get; set; }
  5. //Error CS8341 Auto-implemented instance properties in readonly structs must be readonly.
  6. public int Bar { get; set; }
  7. }
  1. public readonly ref struct Foobar
  2. {
  3. //Error CS8340 Instance fields of readonly structs must be readonly.
  4. public int Foo;
  5. //Error CS8340 Instance fields of readonly structs must be readonly.
  6. public int Bar;
  7. }

五、ref 结构体字段

我们可以在ref结构体的字段成员前添加ref关键字使之返回一个引用。除此之外,我们还可以进一步添加readonly关键字创建“只读引用字段”,并且这个readonly关键可以放在ref后面(ref readonly),也可以放在ref前面(readonly ref),还可以前后都放(readonly ref readonly)。如果你之前没有接触过ref字段,是不是会感到很晕?希望一下的内容能够为你解惑。上面的代码片段定义了一个名为RefStruct的ref 结构体,定义其中的四个字段(Foo、Bar、Baz和Qux)都是返回引用的ref 字段。除了Foo字段具有具有可读写的特性外,我们采用上述三种不同的形式将其余三个字段定义成“自读”的。

  1. public ref struct RefStruct
  2. {
  3. public ref KV Foo;
  4. public ref readonly KV Bar;
  5. public readonly ref KV Baz;
  6. public readonly ref readonly KV Qux;
  7. public RefStruct(ref KV foo, ref KV bar, ref KV baz, ref KV qux)
  8. {
  9. Foo = ref foo;
  10. Bar = ref bar;
  11. Baz = ref baz;
  12. Qux = ref qux;
  13. }
  14. }
  15.  
  16. public struct KV
  17. {
  18. public int Key;
  19. public int Value;
  20. public KV(int key, int value)
  21. {
  22. Key = key;
  23. Value = value;
  24. }
  25. }

1. Writable

在如下的演示代码中,我们针对同一个KV对象的引用创建了RefStruct。在直接修改Foo字段返回的KV之后,由于四个字段引用的都是同一个KV,所以其余三个字段都被修改了。由于Foo字段是可读可写的,所以当我们为它指定一个新的KV后,其他三个字段也被替换了。

  1. KV kv = default;
  2.  
  3. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  4. value.Foo.Key = 1;
  5. value.Foo.Value = 1;
  6. Debug.Assert(kv.Key == 1);
  7. Debug.Assert(kv.Value == 1);
  8. Debug.Assert(value.Foo.Key == 1);
  9. Debug.Assert(value.Foo.Value == 1);
  10. Debug.Assert(value.Bar.Key == 1);
  11. Debug.Assert(value.Bar.Value == 1);
  12. Debug.Assert(value.Baz.Key == 1);
  13. Debug.Assert(value.Baz.Value == 1);
  14. Debug.Assert(value.Qux.Key == 1);
  15. Debug.Assert(value.Qux.Value == 1);
  16.  
  17. value.Foo = new KV(2, 2);
  18. Debug.Assert(kv.Key == 2);
  19. Debug.Assert(kv.Value == 2);
  20. Debug.Assert(value.Foo.Key == 2);
  21. Debug.Assert(value.Foo.Value == 2);
  22. Debug.Assert(value.Bar.Key == 2);
  23. Debug.Assert(value.Bar.Value == 2);
  24. Debug.Assert(value.Baz.Key == 2);
  25. Debug.Assert(value.Baz.Value == 2);
  26. Debug.Assert(value.Qux.Key == 2);
  27. Debug.Assert(value.Qux.Value == 2);

2. ref readonly

第一个字段被定义成“ref readonly”,readonly被置于ref之后,表示readonly并不是用来修饰ref,而是用来修饰引用指向的KV对象,它使我们不能修改KV对象的数据成员。所以如下的代码是不能通过编译的。

  1. KV kv = default;
  2.  
  3. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  4. //Error CS8332 Cannot assign to a member of field 'Bar' or use it as the right hand side of a ref assignment because it is a readonly variable
  5. value.Bar.Key = 2;
  6. //Error CS8332 Cannot assign to a member of field 'Bar' or use it as the right hand side of a ref assignment because it is a readonly variable
  7. value.Bar.Value = 2;

但是这仅仅能够保证我们不能直接通过字段进行修改而已,我们依然可以通过将字段赋值给另一个变量,利用这个变量依然达到更新该字段的目的。

  1. KV kv = default;
  2. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  3.  
  4. kv = value.Bar;
  5. kv.Key = 1;
  6. kv.Value = 1;
  7. Debug.Assert(value.Baz.Key == 1);
  8. Debug.Assert(value.Baz.Value == 1);

由于readonly并不是修饰引用本身,所以我们采用如下的方式通过修改引用达到替换字段的目的。

  1. KV kv = default;
  2. KV another = new KV(1,1);
  3.  
  4. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  5. value.Bar = ref another;
  6. Debug.Assert(value.Bar.Key == 1);
  7. Debug.Assert(value.Bar.Key == 1);

3. readonly ref

如果readonly被置于ref前面,就意味着引用本身,所以针对Baz字段的赋值是不合法的。

  1. KV kv = default;
  2.  
  3. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  4. KV another = new KV(1, 1);
  5. //Error CS0191 A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)
  6. value.Baz = ref another;

但是引用指向的KV对象是可以直接通过字段进行修改。

  1. KV kv = default;
  2. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  3.  
  4. value.Baz.Key = 1;
  5. value.Baz.Value = 1;
  6. Debug.Assert(value.Baz.Key == 1);
  7. Debug.Assert(value.Baz.Key == 1);

4. readonly ref readonly

现在我们知道了ref前后的readonly分别修饰的是字段返回的引用和引用指向的目标对象,所以对于readonly ref readonly修饰的字段Qux,我们既不能字节将其替换成指向另一个KV的引用,也不能直接利用它修改该字段指向的KV对象。

  1. KV kv = default;
  2. var another = new KV(1, 1);
  3. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  4. //Error CS0191 A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)
  5. value.Qux = ref another;
  1. KV kv = default;
  2. var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);
  3. //Error CS8332 Cannot assign to a member of field 'Qux' or use it as the right hand side of a ref assignment because it is a readonly variable
  4. value.Qux.Key = 1;
  5. //Error CS8332 Cannot assign to a member of field 'Qux' or use it as the right hand side of a ref assignment because it is a readonly variable
  6. value.Qux.Value = 1;

除了参数,ref关键字还可以用在什么地方?的更多相关文章

  1. C#中引用类型的变量做为参数在方法调用时加不加 ref 关键字的不同之处

    ​ 一直以为对于引用类型做为参数在方法调用时加不加 ref 关键字是没有区别的.但是今天一调试踪了一下变量内存情况才发现大有不同. 直接上代码,结论是:以下代码是使用了 ref 关键字的版本.它输出1 ...

  2. C# 方法参数传递方式 关键字(in、out、ref)

    in:  值传递,默认传递方式: ref:地址/引用传递,调用时该参数必需已经初始化: out:地址/引用传递,调用时该参数不需要先初始化(被调用方负责该参数的初始化). 注1: in  关键字用于向 ...

  3. c# ref关键字对于引用类型传递的影响

    我们可能见到下面的代码 public static void StringBuilderNoRef(StringBuilder s)     { s.Append(" World" ...

  4. 浅谈c#的三个高级参数ref out 和Params

    c#的三个高级参数ref out 和Params 前言:在我们学习c#基础的时候,我们会学习到c#的三个高级的参数,分别是out .ref 和Params,在这里我们来分别的讲解一下,在这里的我们先不 ...

  5. C#返回多个参数 ref及out

    out 关键字会导致参数通过引用来传递.这与 ref 关键字类似,不同之处在于 ref 要求变量必须在传递之前进行初始化.若要使用 out 参数,方法定义和调用方法都必须显式使用 out 关键字.例如 ...

  6. 浅谈c#的三个高级参数ref out 和Params C#中is与as的区别分析 “登陆”与“登录”有何区别 经典SQL语句大全(绝对的经典)

    浅谈c#的三个高级参数ref out 和Params   c#的三个高级参数ref out 和Params 前言:在我们学习c#基础的时候,我们会学习到c#的三个高级的参数,分别是out .ref 和 ...

  7. C#中ref关键字的用法总结

    ref表示引用的意思,C#中它有多种用法,这里简单总结一下: 1.按引用传递参数 具体可见:C#中的值传递与引用传递(in.out.ref) 2.引用局部变量 引用局部变量指的是在变量声明时使用ref ...

  8. C#基础:值类型、引用类型与ref关键字

    在C#中,ref的意思是按引用传递.可以参考C++: int a = 10, b = 20; void swap(int x, int y) { int temp = x; x = y; y = te ...

  9. ref关键字的作用

    ref关键字使参数按引用传递.其效果是,当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中. 就是说,在调用方法的参数中使用ref关键字可以使得变量能够改变. ref和out都是 ...

  10. ref 关键字out关键字

    using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...

随机推荐

  1. Notion AI:门槛更低的ChatGPT Plus

    [2023年3月27日]由于接口成本的问题,如今的大部分应用应该都只会建立在GPT-3/ChatGPT接口的基础上,所以想要体验GPT-4,还是得尊贵的ChatGPT Plus. 前段日子体验了Not ...

  2. 一文吃透Arthas常用命令!

    Arthas 常用命令 简介 Arthas 是Alibaba开源的Java诊断工具,动态跟踪Java代码:实时监控JVM状态,可以在不中断程序执行的情况下轻松完成JVM相关问题排查工作 .支持JDK ...

  3. SQL concat_ws, collect_set, 和explode合并使用

    1. 背景 有一个这样的数据集:字段和字段的值是两列 目的是将这个数据转换成规整的一个特征是一列的数据: 2. 做法 第一步:先造出列 select ucid ,CASE WHEN type ='性别 ...

  4. 如何玩转国产神器:接口一体化协作平台Apifox!

    前言:Apifox是什么? 简介: 简单来说,Apifox = swagger + mock + postman+Jmeter,是API 文档.API 调试.API Mock.API 自动化测试一体化 ...

  5. springboot-poi ---封装注解式导入导出

    此demo 是基于poi封装对象式注解导入导出,项目框架为springboot项目! 简单的说明一下此demo涉及到的知识点,希望能给初学者带来方便! poi-excel 基本操作(工具) 自定义注解 ...

  6. 【FAQ】统一扫码服务常见问题及解答

    1.隐私政策是怎么样的?收集哪些信息? 关于Scan Kit的隐私政策及收集的信息,请查看SDK隐私安全说明. Android:SDK隐私安全说明 iOS:SDK隐私安全说明 2.如何使用多码识别?多 ...

  7. if, if else, else if 的区别,以js为例

    一个具有迷惑性的例子,底下的两种情况可以看做两个循环,第一个只要满足一个条件就跳出循环,第二个是不管满足几个条件,都会往下走直至循环结束. 1 var a = 5; 2 if (a > 3) { ...

  8. Centos7.x 更换Jenkins构建目录

    原由:最近因为原来的Jenkins构建目录,已经要满了,想着更换下构建目录,此篇文件简单介绍下更换过程. 注:此文章可能仅适用于我个人,仅供参考.如有其他办法,欢迎评论指教. 查了几种方法,最终选为使 ...

  9. mysql 结合python一些日常写法

    python sql语句in写法 sql = "SELECT * FROM user WHERE name in ({})".format(','.join(["'%s' ...

  10. celery+Rabbit MQ简单的Demo

    介绍 一个简单的celery + rabbitmq 的搭建例子,用于记录 Celery 异步处理框架, 安装命令 pip install celery RabbitMQ 消息中间件,用来做队列 安装配 ...