Where is the ref keyword used?

Where is the ref keyword used? There are 5 places: 1. Parameter 2, array index 3, method 4, ref structure 5, ref structure field.

1. Parameters

If the ref keyword is added to the parameters of the method (whether it is a value type or a reference type), it means that the address of the variable is passed to the method as a parameter. The target method can not only directly manipulate the original variable by using the ref parameter, but also directly replace the value of the entire variable. The following code snippet defines a structure-based Record type Foobar, and defines the Update and Replace methods, which have the only parameter type of Foobar, and are prefixed with the ref keyword.

static void Update(ref Foobar foobar)
{
    foobar.Foo = 0;
}

static void Replace(ref Foobar foobar)
{
    foobar = new Foobar(0, 0);
}

public record struct Foobar(int Foo, int Bar);

The modification and replacement of the original variable based on the ref parameter is reflected in the demo code shown below.

var foobar = new Foobar(1, 2);
Update(ref foobar);
Debug.Assert(foobar.Foo == 0);
Debug.Assert(foobar.Bar == 2);

Replace(ref foobar);
Debug.Assert(foobar.Foo == 0);
Debug.Assert(foobar.Bar == 0);

ref + Type (ref Foobar) in C# will be converted to a special reference type Type& in IL. The following is the declaration of the above two methods for IL. It can be seen that their parameter types are both Foobar&.

.method assembly hidebysig static
	void '<<Main>$>g__Update|0_0' (
		valuetype Foobar& foobar
	) cil managed

.method assembly hidebysig static
	void '<<Main>$>g__Replace|0_1' (
		valuetype Foobar& foobar
	) cil managed

2. Array index

We know that an array maps a continuous memory space, and elements with the same byte length are "tiled" on this memory. We can use the index to extract an element of the array. If the index operator prepends the ref key value, then what is returned is the reference/address of the index itself. Similar to the ref parameter, we can use ref array[index] to not only modify the array element pointed to by the index, but also directly replace the array element.

var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) };

Update(ref array[1]);
Debug.Assert(array[1].Foo == 0);
Debug.Assert(array[1].Bar == 2);

Replace(ref array[1]);
Debug.Assert(array[1].Foo == 0);
Debug.Assert(array[1].Bar == 0);

Since the ref keyword is converted into a "reference type" in IL, the corresponding "value" can only be stored in a variable of the corresponding reference type, and the reference variable is also declared through the ref keyword. The following code demonstrates two different variable assignments, the former assigns the "value" of the first element of the Foobar array to the variable foobar (of type Foobar), and the latter assigns the address of the first element in the array to Variable foobarRef (of type Foobar&).

var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) };
Fobar fobar = array[0];
ref Foobar foobarRef = ref array[0];

or

var foobar = array[0];
ref var foobarRef = ref array[0];

The above C# code will be converted into the following IL code. We can not only see the difference in the types declared by foobar and foobarRef (Foobar and Foobar&), but also the difference in the IL instructions used by array[0] and ref array[0]. The former uses ldelem (Load Element) The latter is using ldelema (Load Element Addess).

.method private hidebysig static
	void '<Main>$' (
		string[] args
	) cil managed
{
	// Method begins at RVA 0x209c
	// Header size: 12
	// Code size: 68 (0x44)
	.maxstack 5
	.entrypoint
	.locals init (
		[0] valuetype Foobar[] 'array',
		[1] valuetype Foobar foobar,
		[2] valuetype Foobar& foobarRef
	)

	// {
	IL_0000: ldc.i4.3
	// (no C# code)
	IL_0001: newar Foobar
	IL_0006: dup
	IL_0007: ldc.i4.0
	// Foobar[] array = new Foobar[3]
	// 	{
	// 		new Foobar(1, 1),
	// 		new Foobar(2, 2),
	// 		new Foobar(3, 3)
	// 	};
	IL_0008: ldc.i4.1
	IL_0009: ldc.i4.1
	IL_000a: newobj instance void Foobar::.ctor(int32, int32)
	IL_000f: Stelem Foobar
	IL_0014: dup
	IL_0015: ldc.i4.1
	IL_0016: ldc.i4.2
	IL_0017: ldc.i4.2
	IL_0018: newobj instance void Foobar::.ctor(int32, int32)
	IL_001d: stelem Foobar
	IL_0022: dup
	IL_0023: ldc.i4.2
	IL_0024: ldc.i4.3
	IL_0025: ldc.i4.3
	IL_0026: newobj instance void Foobar::.ctor(int32, int32)
	IL_002b: stelem Foobar
	IL_0030: stloc.0
	// Fobar fobar = array[0];
	IL_0031: ldloc.0
	IL_0032: ldc.i4.0
	IL_0033: ldelem Foobar
	IL_0038: stloc.1
	// ref Foobar reference = ref array[0];
	IL_0039: ldloc.0
	IL_003a: ldc.i4.0
	IL_003b: Fouber's problem
	IL_0040: stloc.2
	// (no C# code)
	IL_0041: no
	// }
	IL_0042: nope
	IL_0043: ret
} // end of method Program::'<Main>$'

3. Method

A method can return a reference/address, such as a reference/address of a variable or an array element, through the preceding ref keyword. As shown in the code snippet below, the method ElementAt returns the address of the specified index in the specified Foobar array. Since this method returns the address of the array element, we use the return value to directly modify the corresponding array element (call the Update method), or directly replace the entire element (call the Replace method). If we look at the IL-based declaration of ElementAt, it also returns Foobar&

var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) };

var copy = ElementAt(array, 1);
Update(ref copy);
Debug.Assert(array[1].Foo == 2);
Debug.Assert(array[1].Bar == 2);
Replace(ref copy);
Debug.Assert(array[1].Foo == 2);
Debug.Assert(array[1].Bar == 2);

ref var self = ref ElementAt(array, 1);
Update(ref self);
Debug.Assert(array[1].Foo == 0);
Debug.Assert(array[1].Bar == 2);
Replace(ref self);
Debug.Assert(array[1].Foo == 0);
Debug.Assert(array[1].Bar == 0);


static ref Foobar ElementAt(Foobar[] array, int index) => ref array[index];

Fourth, the ref structure

If the prefix ref keyword is added when defining the structure, then it is transformed into a ref structure. The fundamental difference between the ref structure and the regular structure is that it cannot be allocated on the heap, and it is always used as a reference, and there will never be a "copy". The most important ref structure is Span<T > gone. The following Foobar structure is a ref structure containing two data members.

public ref struct Foobar{
    public int Foo { get; }
    public int Bar { get; }
    public Foobar(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }
}

The ref structure has many usage constraints. Many people don't understand these constraints very well. In fact, we only need to know that these constraints are ultimately to ensure that the ref structure can only exist on the current thread stack and cannot be transferred to the heap. Based on this principle, let's take a look at the restrictions on the use of the ref structure.

1. Cannot be used as a generic parameter

Unless we can explicitly constrain the generic parameter to the ref structure, and the corresponding method operates the corresponding parameter or variable strictly according to the standard of the ref structure, we can use the ref structure as a generic parameter. Otherwise, for the generic structure, the methods involved will definitely treat it as a regular structure. If the ref structure is specified as a generic parameter type, there will be problems. However, there is no generic constraint for the ref structure yet, so we cannot use the ref structure as a generic parameter, so create a Wrapper<Foobar> as follows (Foobar is the ref structure defined above, and will not be used below Note separately) code is not compilable.

// Error	CS0306	The type 'Foobar' may not be used as a type argument
var wrapper = new Wrapper<Foobar>(new Foobar(1, 2));

public class Wrapper<T>
{
    public Wrapper(T value) => Value = value;
    public T Value { get; }
}

2. Cannot be used as an array element type

The array is allocated on the heap, we naturally cannot use the ref structure as the element type of the array, so the following code will also encounter compilation errors.

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

3. Cannot be used as type and non-ref structure data members

Since class instances are allocated on the heap, and regular structures do not have the constraints of pure stack allocation, ref structures cannot naturally be used as their data members, so the definitions of classes and structures shown below are illegal.

public class Foobarbaz
{
    //Error	CS8345	Field or auto-implemented property cannot be of type 'Foobar' unless it is an instance member of a ref struct.
    public Foobar Foobar { get; }
    public int Baz { get; }
    public Foobarbaz(Foobar foobar, int baz)
    {
        Foobar = foobar;
        Baz = baz;
    }
}

or

public structure Foobarbaz
{
    //Error CS8345 Field or auto-implemented property cannot be of type 'Foobar' unless it is an instance member of a ref struct.
    public Foobar Foobar { get; }
    public int Baz { get; }
    public Foobarbaz(Foobar foobar, int baz)
    {
        Foobar = foobar;
        Baz = baz;
    }
}

4. Cannot implement the interface

When we use a structure in the form of an interface, it will cause boxing and eventually lead to heap allocation, so the ref structure cannot implement any interface.

//Error    CS8343    'Foobar': ref structs cannot implement interfaces
public ref struct Foobar : IEquatable<Foobar>
{
    public int Foo { get; }
    public int Bar { get; }
    public Foobar(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }

    public bool Equals(Foobar other) => Foo == other.Foo && Bar == other.Bar;
}

5. Cannot cause boxing

All types are derived from object by default, and all value types are derived from ValueType, but these two types are reference types (ValueType itself is a reference type), so converting the ref structure to object or ValueType will cause boxing, yes Failed to compile.

//Error	CS0029	Cannot implicitly convert type 'Foobar' to 'object'
Object obj = new Foobar(1, 2);

//Error	CS0029	Cannot implicitly convert type 'Foobar' to 'System.ValueType'
ValueType value = new Foobar(1, 2);

6. Cannot be used in delegates (or lambda expressions)

The variables of a ref struct always refer to the stack address where the struct is stored, so they only have meaning within the method that created the ref struct. They naturally "disappear" once the method returns and the stack frame is reclaimed. A delegate is considered an operation to be performed, and we cannot constrain them to be executed in a certain method, so the ref structure cannot be referenced in the operation performed by the delegate. From another point of view, once the reference to the existing variable is involved in the delegation, it will inevitably lead to the creation of a "closure", that is, a type will be created to encapsulate the referenced variable, which naturally violates the "cannot Use ref struct as class member" constraint. This constraint also applies to Lambda expressions and native methods.

public class Program
{
    static void Main()
    {
        var foobar = new Foobar(1, 2);
        //Error CS8175  Cannot use ref local 'foobar' inside an anonymous method, lambda expression, or query expression
        Action action1 = () => Console.WriteLine(foobar);

        //Error CS8175  Cannot use ref local 'foobar' inside an anonymous method, lambda expression, or query expression
        void Print() => Console.WriteLine(foobar);
    }
}

7. Cannot be in async/await asynchronous method

This constraint is similar to the previous one. Generally speaking, if an await statement is encountered during the execution of an asynchronous method, the bytes will be returned, and it is naturally illegal to have references to the ref structure for subsequent operations. On the other hand, async/await will eventually be converted into a state machine-based type, and there will still be cases where reference variables are encapsulated with automatically generated types, which also violates the constraint of "ref structure cannot be used as a class member".

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

It is worth mentioning that for an asynchronous method whose return type is Task, if the async keyword is not used, since it is an ordinary method, the compiler will not perform state machine-based code generation, so the ref structure can be used freely body.

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

8. Cannot be used in an iterator

Guess you like

Origin blog.csdn.net/yetaodiao/article/details/131554595