C# in,out,ref关键字及协变逆变

  • ref 修饰符,指定参数由引用传递,可以由调用方法读取或写入。
  • out 修饰符,指定参数由引用传递,必须由调用方法写入。
  • in 修饰符,指定参数由引用传递,可以由调用方法读取,但不可以写入。

in关键字

in修饰符通过引用传递参数。 它让形参成为实参的别名,即对形参执行的任何操作都是对实参执行的。它类似于 ref 或 out 关键字,不同之处在于 in 参数无法通过调用的方法进行修改。

struct Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
}

public static void Modify(in Product product)
{
    //product = new Product();          
// 错误 CS8331 无法分配到 变量 'in Product',因为它是只读变量
    //product.ProductName = "测试商品";  
// 错误 CS8332 不能分配到 变量 'in Product' 的成员,因为它是只读变量
    Console.WriteLine($"Id: {product.ProductId}, Name: {product.ProductName}"); 
// OK
}

引入 in ,out,ref参数的原因

我们知道,结构体实例的内存在栈(stack)上进行分配,所占用的内存随声明它的类型或方法一起回收,所以通常在内存分配上它是比引用类型占有优势的。

但是对于有些很大(比如有很多字段或属性)的结构体,将其作为方法参数,在紧凑的循环或关键代码路径中调用方法时,复制这些结构的成本就会很高。当所调用的方法不修改该参数的状态,使用新的修饰符 in ,out或ref声明参数以指定此参数可以按引用安全传递,可以避免(可能产生的)高昂的复制成本,从而提高代码运行的性能。

使用 in 参数需要注意的地方

struct MyNormalStruct
{
    public int Value { get; set; }

    public void UpdateValue(int value)
    {
        Value = value;
    }
}

class Program
{
    static void UpdateMyNormalStruct(MyNormalStruct myStruct)
    {
        myStruct.UpdateValue(8);
    }

    static void Main(string[] args)
    {
        MyNormalStruct myStruct = new MyNormalStruct();
        myStruct.UpdateValue(2);
        UpdateMyNormalStruct(myStruct);
        Console.WriteLine(myStruct.Value);
    }
}

正确输出结果是 2

修改UpdateMyNormalStruct方法的参数加上 in 修饰符

static void UpdateMyNormalStruct(in MyNormalStruct myStruct)
{
    myStruct.UpdateValue(8);
}

结果依然为 2 !

C# 无法知道当它调用一个结构体上的方法(或getter)时,是否也会修改它的值/状态。于是,它所做的就是创建所谓的“防御性副本”。当在结构体上运行方法(或getter)时,它会创建传入的结构体的副本,并在副本上运行方法。这意味着原始副本与传入时完全相同,调用者传入的值并没有被修改。

总结in关键字

  • 使用 in 参数,有助于明确表明此参数不可修改的意图。
  • 只读结构体(readonly struct的大小大于 IntPtr.Size 时,出于性能原因,应将其作为 in 参数传递。
  • 不要将一般(非只读)结构体作为 in 参数,因为结构体是可变的,反而有可能对性能产生负面影响,并且可能产生晦涩难懂的行为。

out关键字

1)带有out的形参,在函数定义时,return前必须给函数赋一个值。
2)调用函数时,带有out的参数不必赋一个初始值。
3)out形参传值是通过引用(by reference)

ref关键字

ref关键字用于改变参数传递,将by value修改为by reference传值,原来是by reference传递的,加上ref还是不加ref,效果是一样的。

ref和out的实质性区别

答案就在其修饰的实参和形参在地址空间的关系上。

1.对于ref,它修饰的形参会指向实参的地址,这样,在方法内对形参进行运算时,改变的就是实参地址上的值。(这就是为什么ref修饰的实参必须要赋初始值,因为这样,ref修饰的实参才能在内存中拥有自己的地址空间。在这里,大家可能对变量地址的分配有些疑问,那么我来补充一下:静态变量--全局变量 在定义时就会得到内存空间,而非静态变量--局部变量 直到赋给初始值时,才会得到内存空间)

2.对于out,它修饰的实参会指向形参的地址,这样,在方法内对形参进行运算时,改变的就是形参地址上的值。(这就是为什么out修饰的实参可以不赋初始值,因为out修饰的实参指向形参的地址空间)

3.对于普通的实参和形参来说,它们之间的关系发生在值上。简单来说,就是形参将实参的值拿来当作自己的值,而形参在方法内的运算就和实参没有什么关系了。

看以下这个例子:

    void Start()
    {
        Dog dog = null;

        CreateDog(dog);
        Debug.LogError(dog == null);
    }

    void CreateDog(Dog dog)
    {
        dog = new Dog();
    }

//------------------------分割线------------------------

    void Start()
    {
        Dog dog = null;

        CreateDog(ref dog);
        Debug.LogError(dog == null);
    }

    void CreateDog(ref Dog dog)
    {
        dog = new Dog();
    }

//------------------------分割线------------------------

    void Start()
    {
        Dog dog = null;

        CreateDog(out dog);
        Debug.LogError(dog == null);
    }

    void CreateDog(out Dog dog)
    {
        dog = new Dog();
    }

上述打印结果依次为true和false和false。

逆变和协变

msdn 解释如下:

协变是指能够使用与原始指定的派生类型相比,派生程度更大的类型。

逆变则是指能够使用派生程度更小的类型。

解释的很正确,大致就是这样,不过不够直白。

直白的理解:

“协变”->”和谐的变”->”很自然的变化”->string->object :协变。

“逆变”->”逆常的变”->”不正常的变化”->object->string 逆变。

手动添加两个类:

    public abstract class Animal
    {
    }
    
    public class Dog : Animal
    {
        
    }

 Dog变成Animal 就是和谐的变化(协变),而如果Animal 变成Dog就是不正常的变化(逆变)

        Dog dog = new Dog();
        Animal animal = dog;
        
        List<Dog> dogList = new List<Dog>();
        List<Animal> animalList = dogList;

因为Dog继承自Animal,所以Animal animal = dog; dog会隐式的转变为Animal类型。

但是List<Dog> 不继承List<Animal> 所以

List<Animal> animalList = dogList无法编译通过。如果想要转换的话,应该使用下面的代码:

List<Animal> animalList = dogList.Select(d => (Animal)d).ToList();

正因如此,所以微软新增了两个关键字:out,in,下面是他们的msdn解释:

image

image

out 和in 关键字在接口和委托中使用,微软使用out 和 in 标记的接口和委托大致如下:

image

image

先看下第一个IEnumerable<T>

image

和刚开始说的一样,T 用out 标记,所以T代表了输出,也就是只能作为结果返回。

承接上文,以下写法便可以正常编译通过:

 IEnumerable<Animal> animalEnumerable = dogList;

上面演示的是协变,接下来要演示下逆变。

为了演示逆变,那么就要找个in标记的接口或者委托了,最简单的就是:

clip_image002

Action<Animal> actionAnimal = new Action<Animal>(a => {/*让动物叫*/ });

Action<Dog> actionDog = actionAnimal;

actionDog(dog);

很明显actionAnimal 是让动物叫,因为Dog是Animal,那么既然Animal 都能叫,Dog肯定也能叫。

in关键字:逆变,代表输入,代表着只能被使用,不能作为返回值,所以C#编译器可以根据in关键字推断这个泛型类型只能被使用,所以Action<Dog> actionDog = actionAnimal;可以通过编译器的检查。

再次演示out关键字:

添加两个类:

    public interface IMyList<out T>
    {
        T GetElement();
    }
    
    public class MyList<T> : IMyList<T>
    {
        public T GetElement()
        {
            return default(T);
        }
    }

因为out 关键字,所以下面的代码可以通过编译

IMyList<Dog> myDogs = new MyList<Dog>();
IMyList<Animal> myAnimals = myDogs;

修改上面两个类为:

    public interface IMyList<out T>
    {
        T GetElement();

        void ChangeT(T t);
    }
    
    public class MyList<T> : IMyList<T>
    {
        public T GetElement()
        {
            return default(T);
        }

        public void ChangeT(T t)
        {
            
        }
    }

则无法通过编译,

 因为T被out修饰,所以T只能作为参数。

同样修改两个类如下,使用in关键字:

    public interface IMyList<in T>
    {
        T GetElement();

        void ChangeT(T t);
    }
    
    public class MyList<T> : IMyList<T>
    {
        public T GetElement()
        {
            return default(T);
        }

        public void ChangeT(T t)
        {
            
        }
    }

 因为用in关键字标记,所以T只能被使用,不能作为返回值。

最后修改为:

    public interface IMyList<in T>
    {
        void ChangeT(T t);
    }
    
    public class MyList<T> : IMyList<T>
    {
        public void ChangeT(T t)
        {
            
        }
    }

编译成功,因为in代表了逆变,所以

IMyList<Animal> myDogs = new MyList<Animal>();
IMyList<Dog> myAnimals = myDogs;

可以编译成功!

你还在哪些地方用到过in,out,ref关键字呢?欢迎评论区补充。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值