C# 7中针对引用类型的ref返回

12

我正在查看使用C# 7的新特性,并使用ref locals & returns功能的一些代码。

对于值类型(value-types),这似乎非常简单,其中ref local变量获取引用(到实际存储),并更新该值将更新原始项的值。

关于引用类型(reference-types)的ref locals如何工作,需要一点解释。我指向下面代码的最后一行:

// A simple class
public class CoolClass
{
    public string Name { get; set; }
    public int Id { get; set; }

    public CoolClass(string name, int id) => (Name, Id) = (name, id);
}

//Dummy method that returns first element with Id > 100
public CoolClass GetSpecialItem_Old(CoolClass[] arr)
{
    for (int i = 0; i < arr.Length; i++)
        if (arr[i].Id > 100)
        {
            return arr[i];
        }
    throw new Exception("Not found");
}

//Same as the other one, but returns by ref C# 7
public ref CoolClass GetSpecialItem_New(CoolClass[] arr)
{
    for (int i = 0; i < arr.Length; i++)
        if (arr[i].Id > 100)
        {
            return ref arr[i];
        }
    throw new Exception("Not found");
}

public void TestMethod()
{
    var arr = new CoolClass[]
    {
        new CoolClass("A", 10),
        new CoolClass("B", 101),
        new CoolClass("C", 11)
    };

    var byVal = GetSpecialItem_Old(arr); //gets back arr[1]
    byVal.Name = "byVal"; //arr[1] = { "byVal", 101 }
    byVal = new CoolClass("newByVal", 25); //arr[1] = { "byVal", 101 }

    ref var byRef = ref GetSpecialItem_New(arr); //gets back arr[1]
    byRef.Name = "byRef"; //arr[1] = { "byRef", 101 }
    //Here it has a different behaviour 
    byRef = new CoolClass("newByRef", 50); //arr[1] = { "newByRef", 50 }
}

2
ref var byRef 基本上是对 arr[1] 的同一引用,因此如果你 byRef = new ...,在底层实际上是 arr[1] = new ... - Caramiriel
2个回答

22
C#最初的设计者将该特性命名为"ref",我认为这是个糟糕的想法。这会让人们混淆引用类型和"ref"参数/返回值。更好的方法是将"ref"视为"别名"。也就是说,ref为现有变量提供了另一个名称。
在您的程序中,无论arr[1]是值类型还是引用类型,byRef都是arr[1]的另一个名称。如果arr[1]是字符串变量(记住,数组元素是变量,您可以更改它们),则byRef也是一个字符串变量,并且它是同一个字符串变量的不同名称。
请注意,arr也是一个变量;如果您更改arr的值,则byRef不会跟随更改。它仍然是相同数组的相同插槽的别名,而与arr的值无关。
因此,当您说
ref var byRef = ref GetSpecialItem_New(arr); //gets back arr[1]

然后

byRef.Name = "byRef"; 

与...完全相同

arr[1].Name = "byRef";

当你说

byRef = new CoolClass("newByRef", 50);

那完全相同

arr[1] = new CoolClass("newByRef", 50);

需要注意的是,如果在赋值给byRef之后更改了arr,你仍然会有一个指向原始arr[1]的别名。

再次强调:byRef只是另一种表示arr[1]的方式,它始终使用在赋值byRefarr的值。无论是值类型还是引用类型都没有区别。

相比之下,byVal不是arr[1]的别名。它是一个拥有arr[1]内容副本的第二个变量。当你为byVal赋值时,你并不是在给arr[1]赋值,而是在给一个完全不同的变量byVal赋值。

arr[1]的内容是一个引用,而这个引用被复制到byVal,一个完全独立的存储位置。


3
谢谢你的解释,埃里克。"ref 的更好理解方式是“别名”"和“当 byRef 赋值时,它始终使用其拥有的 arr 值”的这些观点使得思考它变得更容易了。 - Arghya C

0
另一个有趣的问题是,当进行测试时,如何强制Ref returns and ref locals按照您的意愿运行? 您可以使用JustMock模拟GetSpecialItem_New方法来实现这一点。
以下是问题中的方法示例:
public class Foo
{
    //Same as the other one, but returns by ref C# 7
    public ref CoolClass GetSpecialItem_New(CoolClass[] arr)
    {
        for (int i = 0; i < arr.Length; i++)
            if (arr[i].Id > 100)
            {
                return ref arr[i];
            }
        throw new Exception("Not found");
    }
}

以下是如何模拟该方法以返回所需结果进行隔离测试的方法:

[TestMethod]
public void TestCoolClass()
{
    var expected = new CoolClass("42", 42);

    var arr = new CoolClass[]
    {
        new CoolClass("A", 10),
        new CoolClass("B", 101),
        new CoolClass("C", 11)
    };

    // Arrange
    var sut = Mock.Create<Foo>();
    Mock.Arrange(sut, s => s.GetSpecialItem_New(arr)).Returns(LocalRef.WithValue(expected).Handle).OccursOnce();

    // Act
    ref CoolClass actual = ref sut.GetSpecialItem_New(arr);

    // Assert
    Assert.AreEqual(expected, actual);
}

这里有一篇帮助文章详细解释了测试方法。

免责声明。我是JustMock的开发人员之一。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接