声明语句

“声明语句”声明一个新变量,并对其进行初始化(可选)。 所有变量都有已声明类型。 可以参阅有关 .NET 类型系统的文章,详细了解类型。

隐式类型本地变量

在方法范围内声明的变量可以具有隐式“类型”var。 隐式类型本地变量为强类型,就像用户已经自行声明该类型,但编译器决定类型一样。 ab 的以下两个声明在功能上是等效的:

var a = 10; // Implicitly typed.
int b = 10; // Explicitly typed.

重要

在启用可为空引用类型的情况下使用 var 时,即使表达式类型不可为空,也始终表示可为空引用类型。 编译器的 null 状态分析可防止取消引用潜在的 null 值。 如果从未向可能为 null 的表达式分配该变量,编译器将不会发出任何警告。 如果将该变量分配给可能为 null 的表达式,则必须先测试其是否为 null,然后才能将其取消引用,以避免出现任何警告。

var 关键字的常见用途是用于构造函数调用表达式。 使用 var则不能在变量声明和对象实例化中重复类型名称,如下面的示例所示:

var xs = new List<int>();

从 C# 9.0 开始,可以使用由目标确定类型的 new 表达式作为替代方法:

List<int> xs = new();
List<int>? ys = new();

在模式匹配中,在 var 模式中使用 var 关键字。

下面的示例演示两个查询表达式。 在第一个表达式中,允许使用 var,但不是必需的,因为查询结果的类型可以明确表述为 IEnumerable<string>。 不过,在第二个表达式中,var 允许结果是一系列匿名类型,且相应类型的名称只可供编译器本身访问。 如果使用 var,便无法为结果新建类。 在示例 #2 中,foreach 迭代变量 item 必须也为隐式类型。

// Example #1: var is optional when
// the select clause specifies a string
string[] words = { "apple", "strawberry", "grape", "peach", "banana" };
var wordQuery = from word in words
                where word[0] == 'g'
                select word;

// Because each element in the sequence is a string,
// not an anonymous type, var is optional here also.
foreach (string s in wordQuery)
{
    Console.WriteLine(s);
}

// Example #2: var is required because
// the select clause specifies an anonymous type
var custQuery = from cust in customers
                where cust.City == "Phoenix"
                select new { cust.Name, cust.Phone };

// var must be used because each item
// in the sequence is an anonymous type
foreach (var item in custQuery)
{
    Console.WriteLine("Name={0}, Phone={1}", item.Name, item.Phone);
}

ref 局部变量

在变量类型前添加 ref 关键字以声明 ref 局部变量。 假设 GetContactInformation 方法声明为 ref 返回

public ref Person GetContactInformation(string fname, string lname)

按值分配会读取变量值,并将它分配给新变量:

Person p = contacts.GetContactInformation("Brandie", "Best");

上面的分配将 p 声明为本地变量。 它的初始值是通过读取 GetContactInformation 返回的值进行复制。 之后对 p 的任何分配都不会更改 GetContactInformation 返回的变量值。 变量 p 不再是返回的变量的别名。

声明 ref 变量,复制原始值的别名。 在下面的分配中,p 是从 GetContactInformation 返回的变量的别名。

ref Person p = ref contacts.GetContactInformation("Brandie", "Best");

后续使用 p 等同于使用 GetContactInformation 返回的变量,因为 p 是此变量的别名。 对 p 所做的更改也会更改从 GetContactInformation 返回的变量。

可通过相同方式按引用访问值。 在某些情况下,按引用访问值可避免潜在的高开销复制操作,从而提高性能。 例如,以下语句显示用户可如何定义一个用于引用值的 ref 局部变量值。

ref VeryLargeStruct reflocal = ref veryLargeStruct;

ref 关键字用于局部变量声明前面和第二个示例中的值前面。 在这两个示例中,如果无法同时将 ref 关键字包含在变量声明和赋值中,则会导致编译器错误 CS8172:“无法使用值对按引用变量进行初始化”。

ref VeryLargeStruct reflocal = ref veryLargeStruct; // initialization
refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

Ref 局部变量仍必须在声明时进行初始化。

下列示例定义存储整数值数组的 NumberStore 类。 FindNumber 方法按引用返回第一个大于或等于作为参数传递的数字的数字。 如果没有大于或等于该参数的数字,则方法返回索引 0 中的数字。

using System;

class NumberStore
{
    int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

    public ref int FindNumber(int target)
    {
        for (int ctr = 0; ctr < numbers.Length; ctr++)
        {
            if (numbers[ctr] >= target)
                return ref numbers[ctr];
        }
        return ref numbers[0];
    }

    public override string ToString() => string.Join(" ", numbers);
}

下列示例调用 NumberStore.FindNumber 方法来检索大于或等于 16 的第一个值。 然后,调用方将该方法返回的值加倍。 示例输出表明,NumberStore 实例的数组元素值反映了更改。

var store = new NumberStore();
Console.WriteLine($"Original sequence: {store.ToString()}");
int number = 16;
ref var value = ref store.FindNumber(number);
value *= 2;
Console.WriteLine($"New sequence:      {store.ToString()}");
// The example displays the following output:
//       Original sequence: 1 3 7 15 31 63 127 255 511 1023
//       New sequence:      1 3 7 15 62 63 127 255 511 1023

如果引用返回值不受支持,需要通过返回数组元素及其值的索引来执行此类操作。 然后,调用方可使用此索引修改单个方法调用中的值。 但调用方也可修改要访问的索引,还可修改其他数组值。

下面的示例展示了如何将 FindNumber 方法重写为使用 ref 局部重新分配:

using System;

class NumberStore
{
    int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

    public ref int FindNumber(int target)
    {
        ref int returnVal = ref numbers[0];
        var ctr = numbers.Length - 1;
        while ((ctr >= 0) && (numbers[ctr] >= target))
        {
            returnVal = ref numbers[ctr];
            ctr--;
        }
        return ref returnVal;
    }

    public override string ToString() => string.Join(" ", numbers);
}

如果查找的数字更接近数组末尾,则第二个版本使用较长的序列会更有效,因为数组是从末尾向开头迭代,使需要检查的项更少。

编译器对 ref 变量强制实施范围规则:ref 局部变量、ref 参数和 ref struct 类型中的 ref 字段。 这些规则可确保引用在时间上不超过它所引用的对象。 请参阅关于方法参数的文章中有关范围界定规则的部分。

ref 和 readonly

readonly 修饰符可以应用于 ref 局部变量和 ref 字段。 readonly 修饰符会影响其右边的表达式。 请参阅以下示例声明:

ref readonly int aConstant; // aConstant can't be value-reassigned.
readonly ref int Storage; // Storage can't be ref-reassigned.
readonly ref readonly int CantChange; // CantChange can't be value-reassigned or ref-reassigned.
  • “重新赋值”表示重新分配变量的值。
  • “ref 赋值”表示变量现在引用其他对象。

readonly refreadonly ref readonly 声明仅对 ref struct 中的 ref 字段有效。

scoped ref

上下文关键字 scoped 限制值的生存期。 scoped 修饰符将 ref-safe-to-escape 或 safe-to-escape 生存期分别限制为当前方法。 实际上,添加 scoped 修饰符可确保代码不会延长变量的生存期。

可以将 scoped 应用于参数或局部变量。 当类型为 ref struct 时,scoped 修饰符可以应用于参数和局部变量。 否则,scoped 修饰符只能应用于 ref 类型的局部变量。 这包括使用 ref 修饰符声明的局部变量以及使用 inrefout 修饰符声明的参数。

当类型为 ref struct 时,使用 structout 参数和 ref 参数声明的方法将 scoped 修饰符隐式添加到 this

请参阅