zoukankan      html  css  js  c++  java
  • 深入剖析引用参数Ref和Out

    [如果您正准备阅读本文,请一定决心读完所有的评论,谢谢。]

    学过C/C++的人,对C#的关键字Ref和Out应该都很好理解。它们都提供了一种可以在被调用函数内修改传递的参数的值的方法。因为这一功能很类似C/C++的指针。对于没学过C/C++的,也应该可以明白这两个参数的作用。

    虽然Ref和Out都提供了修改参数值的方法,但它们还是有一点点小的区别。

    1、Ref在作为参数调用函数之前,变量一定要赋值,否则会得到一个常规编译错误:使用了未赋值的变量。
    2、在被调用函数内,以Ref引入的参数在返回前不必为它赋值。
    3、Out在作为参数调用函数之前,变量可以不被赋值。
    4、在被调用函数内,以Out引入的参数在返回前一定要至少赋值一次。

    其实本质上讲,Ref更适合理解为给被调用函数传递了一个与原参考同地址的变量。而Out则可以理解为在调用函数前,先给变量找个地方,让被调用函数在给定地点放一个值。

    看上去很简单不是吗?确实如此,这里是一个例子:
    namespace StudyAndTest
    {
        
    /// <summary>
        
    /// Summary description for Class1.
        
    /// </summary>

        class Class1
        
    {
            
    /// <summary>
            
    /// The main entry point for the application.
            
    /// </summary>

            [STAThread]
            
    static void Main(string[] args) 
            
    {
                
    int m_temp        =0;    //Must be assigned to before call any mothed with the variable by reference.
                Console.WriteLine("Int data befor change:{0}",m_temp);
                ChangeData1(
    ref m_temp);
                Console.WriteLine(
    "Int data after change:{0}",m_temp);
                ChangeData2(
    out m_temp);
                Console.WriteLine(
    "Int data after change:{0}",m_temp);
            }


            
    static void ChangeData1(ref int i_ref)
            
    {
                Console.WriteLine(
    "Int data in ChangeData1:{0}",i_ref);
                i_ref    
    = 1;
            }


            
    static void ChangeData2(out int i_ref)
            
    {
                
    //Console.WriteLine("Int data in ChangeData2:{0}",i_ref);    //Error in building, use of unassigned local variable i_ref
                i_ref    = 2;    //The out parament i_ref must be assigned to before control leaves the current mothod.
            }

        }

    }

    然而C#毕竟是与C/C++有着不同之处的。这就是在C#内,所有的变量被分为两类:值类型和引用类型。
    那么我们就会有这样的问题:将Ref和Out分别应用于引用类型和值类型的变量上,会是什么样的结果呢?

    对于应用于值类型数据的情况,上面的例子已经完全讨论过了,就是完全遵守上面的四句话。而对于引用类型数据,有一个很有趣的问题,就是默认情况下(不带Ref也不带Out)它是以Ref情况而调用函数,即上面的四句话仍然满足。

    看这样的一个例子:

    namespace StudyAndTest
    {
        
    /// <summary>
        
    /// Summary description for Class1.
        
    /// </summary>

        class Class1
        
    {
            
    /// <summary>
            
    /// The main entry point for the application.
            
    /// </summary>

            [STAThread]
            
    static void Main(string[] args) 
            
    {
                TempClass m_class1    
    = new TempClass();
                m_class1.m_member    
    = 0;
                Console.WriteLine(
    "i_obj data before changeData3 :{0}",m_class1.m_member);
                ChangeData3(m_class1);
                Console.WriteLine(
    "i_obj data after changeData3 :{0}",m_class1.m_member);

            }

            
    static void ChangeData3(TempClass i_obj)
            
    {
                i_obj.m_member    
    = 3;
    //            Console.WriteLine("i_obj data in ChangeData3:{0}",i_obj.m_member);
            }

        }


        
    class TempClass
        
    {
            
    public int m_member;
        }

    }


    这让人感觉就是Ref,确实如此,默认就是在以Ref为引用类型在调用函数,所以还是要注意以下问题:
    引用类型数据一定要初始化。而至于引用类型自己初始化的问题,就交给该类型自己了。如上面的问题,m_member在没有赋值前,一样可以编译的,但运行一定就不对了(但有默认值),这是因为TempClass里没有构造函数。在被调用函数内,一样的使用参数,而且所有对引用参数的改变都影响到函数外。这是默认的情况。

    但如果我们强行加上Ref或者Out关键字,会是什么结果呢???
    1、如果是用Ref,那么结果是和什么都没用一样!即默认就是用的Ref。(让我们少打了几个字符)
    2、如果是用Out,那么要遵守上面的3,4原则,即:在调用前,不必初始化引用对象,再简单一点:就是可以不用New一个对象。但在函数内,返回前一定要New一个,并且在New之前,参数对象是不能使用的。
    也就是上面说到的,Out只是在调用前分配了一个地点,在调用函数中使用该地点。注意:这里“地点”一词决不是内存地址。

    再思考一个问题:如果在使用Out参考时,在调用函数前,我们已经New了一个对象,再来调用函数结果会是什么呢?
    你将“丢失”一部份内存(如果在C/C++里,一定是这样的)。也就是说,在调用了函数后,函数里New的一个对象会让函数外的对象丢失,而新的对象在函数内有效,在函数外也有效。幸运的是:原来的对象的内存并不会像C/C++那样完全的丢失,它将由垃圾回收器来管理了。所以我们并不担心内存的真正丢失问题(这真是一件值得庆幸的事)。

    看这样的例子:
        class Class1
        
    {
            
    /// <summary>
            
    /// The main entry point for the application.
            
    /// </summary>

            [STAThread]
            
    static void Main(string[] args) 
            
    {
                TempClass m_class1    
    = new TempClass();
                m_class1.m_member    
    = 0;
                Console.WriteLine(
    "i_obj data before changeData3 :{0}",m_class1.m_member);
                ChangeData3(
    out m_class1);
                Console.WriteLine(
    "i_obj data after changeData3 :{0}",m_class1.m_member);

            }

            
    static void ChangeData3(out TempClass i_obj)
            
    {
                i_obj    
    = new TempClass();
                i_obj.m_member    
    = 3;
    //            Console.WriteLine("i_obj data in ChangeData3:{0}",i_obj.m_member);
            }

        }


        
    class TempClass
        
    {
            
    public int m_member;
        }

    到此为止,或许你已经觉得你已经对Ref和Out已经十分的了解了,然而你可能无法回答下面的一个问题:它的输出是什么?
        class Class1
        
    {
            
    /// <summary>
            
    /// The main entry point for the application.
            
    /// </summary>

            [STAThread]
            
    static void Main(string[] args) 
            
    {
                TempClass m_class1    
    = new TempClass();
                m_class1.m_member    
    = 0;
                TempClass m_class2    
    = m_class1;
                ChangeData4(
    ref m_class2,m_class2);
                Console.WriteLine(
    "m_class1:{0},m_class2:{1}",m_class1.m_member,m_class2.m_member);
            }

            
    static void ChangeData4(ref TempClass i_obj1,TempClass i_obj2)
            
    {
                i_obj1    
    = new TempClass();
                i_obj1.m_member    
    = 41;
                i_obj2    
    = new TempClass();
                i_obj2.m_member    
    = 42;
            }

        }


        
    class TempClass
        
    {
            
    public int m_member;
        }

    请不要猜测它的结果,这样可能会让你犯一个错误。如果你利用前面的知识来理解,认为两个对象输出的结果是同一个对象的结果,那么你这回又是错误的!
    这什么呢?前面不是说的很清楚吗?上面的函数参数一个用了ref,一个没有用,默认的都是Ref,也就是说:两个参数其实是一样的!!而且函数内用了两次New,因此,对象Class1的最后一个有效实例是最后一次New的结果,也就是说:class1和class2都将引用到(指向)函数内最后一次New的实例,所以最后输出应该都是42.

    然而很不幸,这回结果是:m_class1:0,m_class2:41
    而且它们的内存关系也是有点复杂的。让我们先来看看函数调用前的假想内存情况:

    Class1->直接指向TempClass实例1

    Class2->指向Class1,间接指向TempClass实例1

     

    TempClass实例1,成员值:0

     

     

     


    调用函数后的结果: 

    Class1->直接指向TempClass实例1

    Class2->直接指向TempClass实例2,因为第一个参数用的是Ref

     

    TempClass实例1,成员值:0

    TempClass实例2,第一次New的结果,成员值:41

    TempClass实例3,第二次New的结果,但这只是在函数内的一个副本,该副本不能影响到函数外,即出了函数体就丢失,成员值:42

     


    这就是为什么 会有两个结果了!!!也就是说:当我们用Ref来传递引用类型数据的一个引用时(这里的Class2就是这样的情况,它是一个指向TempClass实例1的一个引用),其实是使用的实例的一个副本。即:产生了即没有用Ref也没有用Out的效果。

    [Post之后的修改]
    这里请一定注意这里的调用:  ChangeData4(ref
     m_class2,m_class2);
    读者自己试着这样试试:
      ChangeData4(ref
     m_class1,m_class2);
      ChangeData4(ref
     m_class2,m_class1);
      ChangeData4(ref
     m_class2,m_class1);
    然后再分析一下内存,可能会有很大的收获。

    最后一个问题:就是上面这种情况应用于值类型数据的时候会是什么情况呢?这就交给读者自己去解决了。

    好了,最后一个引用的例子值得思考一下。希望对读者有帮助。

  • 相关阅读:
    一次性能优化最佳实践
    数据库大型应用解决方案总结 笔记
    为ASP.NET MVC创建一个基于Mini容器的ControllerFactory
    EmitMapper,AutoMapper,NLiteMapper和手工映射性能大比拼
    Web 高性能开发汇总
    DI 容器Mini容器工作机制剖析(上篇)
    Struct 创建性能大比拼(反射,泛型反射,泛型创建,缓存Emit)
    Class 创建性能大比拼(反射,泛型反射,泛型创建,缓存Emit,非缓存Emit)
    DI 容器Mini容器工作机制剖析(下篇)
    国内外ubuntu衍生版比较
  • 原文地址:https://www.cnblogs.com/WuCountry/p/338514.html
Copyright © 2011-2022 走看看