C#——各種參數,擴展方法


余近日復習C#之基礎知識,故作一隨筆,也是對此前幾篇博客中所記錄的傳值參數相關內容之匯總,還望諸位加以批評指正。

該博客包括以下內容:

傳值參數

引用參數

輸出參數

數組參數

具名參數

可選參數

擴展方法(this參數)

 

傳值參數

C#語言規范中道:“聲明時不帶修飾符的形參是值形參。一個值形參對應於一個局部變量,只是它的初始值來自該方法調用所提供的相應實參。

當形參是值形參時,方法調用中的對應實參必須是表達式,並且它的類型可以隱式轉換為形參的類型。

允許方法將新值賦給值參數。這樣的賦值只影響由該值形參表示的局部存儲位置,而不會影響在方法調用時由調用方給出的實參。”

 

注意:1、值參數創建變量之副本;2、對參數之改變永遠不影響變量之值

傳值參數→值類型

        static void Main(string[] args)
        {
            int y=100;
            AddOne(y);
            System.Console.WriteLine(y);
        }

        static void AddOne(int x)//此處x便為傳值參數(或值參數)
        {
            x+=1;
            System.Console.WriteLine(x);
        }
        /*
        運行結果:
        101
        100
         */

根據結果顯示,第一行即為AddOne方法內打印出的語句,其結果為101,因為執行了x+=1;,打印出的亦為x之數值;而第二行打印出的結果是在調用方法完后y(變量)之值,我們發現,y(變量)之數值並未發生改變。

其原因便是,我們所修改的是y(變量)傳進來的一個副本,其並不影響變量之值。

 

傳值參數→引用類型,創建新的對象

static void Main(string[] args)
        {
            Student stu=new Student(){Name="Mark"};
            System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name);
            SomeMethod(stu);
        }

        static void SomeMethod(Student stu)//類類型便為典型之引用類型,在此為傳值參數
        {
            stu =new Student(){Name="Mark"};
            System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name);
        }

class Student
    {
        public string  Name { get; set; }
    }
 /*
    運行結果:
    Hashcode=1542680,Name=Mark.
    Hashcode=20054852,Name=Mark.
     */

根據運行結果顯示,第一行打印的便為方法之外變量的hashcode與名字,第二行打印的便為調用方法內打印的hashcode與名字。很顯然,雖然我們給它們設置的名字都是Mark,但它們的hashcode是完全不一樣的。因為第二個是我們在方法內創建的新對象(如果給其方法內的變量賦一個別的名字比如Elliot,那么打印出的名字與hashcode都不一樣,這樣似乎更加的直觀)。

其原因在於,引用類型存儲的是實例之地址。方法外的變量stu存儲了對象之地址,傳入方法里的值也便是對象之地址,而傳值參數傳入方法內的是變量之副本(副本里也儲存的是對象之地址)。我們調用方法后,改變了副本里的值,也便就是改變了副本里之前存的地址,換成了一個新地址,那么自然而然的指向了一個新對象。而對值參數(副本)的改變不會影響變量的值。故方法外之變量依舊指向原來的那個對象,而更改后的副本指向了一個新對象,它們互不影響。

注意:這種現象與實際工作中並無多大意義,我們用方法只是為了讀取值,不會新建個對象引用着。

 

傳值參數→引用類型,不創建對象,只操作對象

 

注意:對象還是那個對象,但對象內的值已經發生改變。

 

    class Program
    {
        static void Main(string[] args)
        {
            Student outterstu=new Student(){Name="Mark"};
            System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name);
            UpdateObject(outterstu);
        }

        static void UpdateObject(Student stu)
        {
            stu.Name="Elliot";//未創建新對象,只是講對象的名字屬性的值改變了。
            System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name);
        }
    }
  class Student
       {
         public string Name { get; set; }
       }
       /*
       運行結果:
        Hashcode=1542680,Name=Mark.
        Hashcode=1542680,Name=Elliot.
        */

根據運行結果顯示,未調用方法前名字為Mark,調用完之后則變成了Elliot。但是,它們的hashcode值卻都是完全一樣的,這說明它們指向的是同一個對象。而調用方法只是將對象內的值做了改動而已。(注意與引用參數情形之下加以區分,且為后話)

注意:這種現象很少見,對方法而言,其主要輸出還是靠返回值。這是該方法的副作用(side-effect)。

 

引用參數(引用形參)

C#語言規范中道:“ ref 修飾符聲明的形參是引用形參。與值形參不同,引用形參並不創建新的存儲位置。相反,引用形參表示的存儲位置恰是在方法調用中作為實參給出的那個變量所表示的存儲位置。

當形參為引用形參時,方法調用中的對應實參必須由關鍵字 ref 並后接一個與形參類型相同的 variable-reference組成。變量在可以作為引用形參傳遞之前,必須先明確賦值。

在方法內部,引用形參始終被認為是明確賦值的。”

 

注意:1、引用參數並不創建變量之副本。2、使用ref修飾符顯示指出——該方法的副作用是為了修改實際參數之值。

 

引用參數→值類型

     static void Main(string[] args)
        {
            int y=100;
            IWantSideEffect(ref y);//ref修飾符顯式指出副作用
            System.Console.WriteLine(y);
        }   

        static void IWantSideEffect(ref int x)
        {
            x+=1;
            System.Console.WriteLine(x);
        }    
        /*運行結果:
            101
            101
         */

與"傳值參數→值類型"一小程序對比可見,這一次方法外的y(變量)值發生了改變。這是因為方法的參數(x)與方法外之變量(y)所指內存之地址是相同的。我們在方法內改變參數所指內存地址中的值,則相當於變量所指內存地址中的值發生改變。那么我們用變量訪問內存地址中存儲的值時,拿到的便是改變后的值。

 

引用參數→引用類型,創建新對象

 class Program
    {
        static void Main(string[] args)
        {
            Student outterstu=new Student(){Name="Mark"};
            System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name);
            System.Console.WriteLine("----------------------------------------------");
            IWantSideEffect(ref outterstu);
            System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name);
        }   

        static void IWantSideEffect(ref Student stu)
        {
            stu =new Student (){Name="Elliot"};
            System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name);
        }
    }

    class Student
    {
        public string Name { get; set; }
    }
    /*
        運行結果:
        Hashcode=1542680,Name=Mark.
        ----------------------------------------------
        Hashcode=20054852,Name=Elliot.
        Hashcode=20054852,Name=Elliot.
    */

根據運行結果顯示:第一行為未調用方法時的變量之Hashcode與Name。分割線下的第一行是調用方法時打印出的Hashcode與Name,第二行是在調用方法完后再次打印變量之Hashcode與Name。我們發現,調用方法后的參數"值"與變量"值"是完全一樣的。

其原因在於,引用類型變量存儲的是對象(或曰實例)在堆內存上之地址。那么當變量傳入方法時,參數中所存也便為對象在對內存上之地址(相同的),在方法內部之邏輯,對參數進行了修改(創建了新的對象),由於這時傳入的不是變量之”副本“,而是真真切切的變量,所以變量中所儲存之值(地址)也隨即發生了改變。因為是創建了新的對象,所以無論是參數還是變量中所存之地址是新對象之在堆內存上之地址,所以它們指向了同一個新對象。

 

引用參數→引用類型,不創建新的對象,只修改對象值

 

    class Program
    {
        static void Main(string[] args)
        {
            Student outterstu=new Student(){Name="Mark"};
            System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name);
            System.Console.WriteLine("------------------------------------------");
            SomeSideEffect(ref outterstu);
            System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name);
        }   

        static void SomeSideEffect(ref Student stu)
        {
            stu.Name="Elliot";
            System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name);            
        }
    }

    class Student
    {
        public string Name { get; set; }
    }
    /*
        運行結果:
        Hashcode=1542680,Name=Mark.
        ------------------------------------------
        Hashcode=1542680,Name=Elliot.
        Hashcode=1542680,Name=Elliot.
    */

根據運行結果顯示:第一行為未調用方法時,外部變量(outterstu)的Hashcode與Name;分割線下面的第一行是方法內部打印出來的,注意此時的Hashcode與外部變量是相同的,但是Name已經改寫了;第二行則是在調用完方法后,再次打印出外部變量之Hashcode與Name。其Hashcode值並未改變,而其Name之值已經改寫了。

與上一段程序相比,這次並沒有創建新對象,只是改變原有對象之值。

到此為止,應注意其與"傳值參數→引用類型,不創建對象,只操作對象"之大不同

 class Program
    {
        static void Main(string[] args)
        {
            Student outterstu=new Student(){Name="Mark"};
            System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name);
            System.Console.WriteLine("------------------------------------------");
            SomeSideEffect(outterstu);
            System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name);
        }   

        static void SomeSideEffect(Student stu)
        {
            stu.Name="Elliot";
            System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name);            
        }
    }

    class Student
    {
        public string Name { get; set; }
    }
    /*
        運行結果:
        Hashcode=1542680,Name=Mark.
        ------------------------------------------
        Hashcode=1542680,Name=Elliot.
        Hashcode=1542680,Name=Elliot.
    */

上面這段代碼,我們將ref關鍵字刪除,運行后之結果並無改變,但其內涵確實極為大不相同的

當為傳值情況時,變量傳進方法時,在內存中創建了其自身之副本,即變量(outterstu)與參數(stu)所指向之內存地址是不一樣的。而在此兩個不同之內存地址之中,卻都保存着相同之實例在堆內存中之地址。

而引用參數情況則為,變量(outterstu)與參數(stu)所指內存地址為同一內存地址,在這同一內存地址中保存的便為實例於堆內存中之地址。

 

輸出參數

C#語言定義文檔道:” out 修飾符聲明的形參是輸出形參。類似於引用形參,輸出形參不創建新的存儲位置。相反,輸出形參表示的存儲位置恰是在該方法調用中作為實參給出的那個變量所表示的存儲位置

當形參為輸出形參時,方法調用中的相應實參必須由關鍵字 out 並后接一個與形參類型相同的 variable-reference組成。變量在可以作為輸出形參傳遞之前不一定需要明確賦值,但是在將變量作為輸出形參傳遞的調用之后,該變量被認為是明確賦值的。

在方法內部,與局部變量相同,輸出形參最初被認為是未賦值的,因而必須在使用它的值之前明確賦值。

在方法返回之前,該方法的每個輸出形參都必須明確賦值。“

通俗點說,比如,方法相當於加工數據之地方,而其返回值便為加工好之產品,但調用后只可產生依次。如若我們想在調用方法后得到多個值,即除了返回值之外還想拿到其他之值,則就要用到輸出參數。

 

注意:1、輸出參數並不創建變量之副本(變量與參數指向相同之內存地址);2、方法體內必須要有對輸出變量之賦值操作;3、使用out修飾符顯式指出——此方法之副作用為通過參數向外輸出值;4、變量初始值可有可空,終歸是要被覆蓋掉的。

在C#里,其本身就帶有Parse與TryParse兩種方法,Parse是解析,TryParse之返回值為布爾類型,判斷解析是否成功,若解析成功后我們想拿到解析后之值,而其返回值被布爾值所占用,此時便用到輸出參數之功能。如圖:

輸出參數→值類型

那么,我們自己給double加一個"偽"TryParse方法。

  class Program
    {
        static void Main(string[] args)
        {
            string arg="123";
            string arg2="asd";
            double x=0;
            double y=0;
            bool b1=DoubleParser.TryParse(arg,out x);
            bool b2=DoubleParser.TryParse(arg2,out y); 

            if (b1==false)
            {
                System.Console.WriteLine("Input Error!");
            }
            else
                System.Console.WriteLine(x);

                System.Console.WriteLine("---------------------");

            if (b2==false)
            {
                System.Console.WriteLine("Input Error!");
                System.Console.WriteLine(y);
            }
        }           
    }

    class DoubleParser
    {
        public static bool TryParse(string input,out double result)
        {
            try
            {
                result =double.Parse (input);
                return true;
            }
            catch
            {
                result=0;//必須給result賦初值,在方法返回之前,該方法的每個輸出形參都必須明確賦值。
                return false;
            }
        }
    }
    /*
        運行結果:
                    123
                    ---------------------
                    Input Error!
                    0
    */

第一行是解析成功之x之值,分割線下面為解析失敗之y值,其原有值亦被覆蓋成0。

 

輸出參數→引用類型

變量有無初始值都可,即便其有初始值,即引用着一個在堆內存中之對象。調用方法后,對參數進行賦值操作,因為變量與參數指向的是同一內存地址,則對參數值修改后(創建新對象),變量也引用上了新創建之對象。

舉例:

有一個"學生工廠",不斷的輸送人才出來,其內部有兩個邏輯,一是判斷成績是否符合要求,一是判斷學生是否有姓名。如果兩條其中有一項不滿足,則為false,人才就流失了;如果均滿足的話,就創造出了人才(即創建一個新對象)。

    class Program
    {
        static void Main(string[] args)
        {
            Student stu=null;
            bool b=StudentFactory.Creat("Elliot",90,out stu);

            if (b==true)
            {
                System.Console.WriteLine("Name is {0},Score is {1}.",stu.Name,stu.Score);
            }
        }           
        
    }

    class Student
    {
        public string Name { get; set; }
        public int Score { get; set; }
    }

    class StudentFactory
    {
        public static bool Creat(string stuName,int stuScore,out Student result)
        {
            result =null;
            
            if (string.IsNullOrEmpty(stuName))//判斷姓名是否為空
            {
                return false;
            }
            
            if (stuScore<60)//判斷成績
            {
                return false;
            }

            result=new Student (){Name=stuName,Score=stuScore};//創建一個新對象 return true;
        }
    }

    /*
       運行結果:
       Name is Elliot,Score is 90.
     */

 

在此,稍作總結,對於ref修飾符來說是專為“改變”;對於out修飾符來說是專為"輸出"

 

數組參數

數組參數聲明時會用params關鍵字修飾,且必須是形參列表中最后一個,且只有一個

直接看例子,我們要計算一個數組內所有元素之和。

static void Main(string[] args)
        {
            int[] arrray={1,2,3,4,5};
            int result=Sum(arrray);
            System.Console.WriteLine(result);
            //運行結果:15
        }           

        static int Sum(int[] intArray)
        {
            int sum=0;
            foreach (var i in intArray)
            {
                sum+=i;
            }
            return sum;

我們在調用這個方法之前,必須有一個已經聲明好的數組才可以放進去,但是,若在方法之參數列表內加一params修飾,就不必這么麻煩了。

        static void Main(string[] args)
        {
            int result=Sum(1,2,3,4,5);//直接輸入數據即可,無需創建數組
            System.Console.WriteLine(result);
            //運行結果:15
        }           

        static int Sum(params int[] intArray)//加params修飾
        {
            int sum=0;
            foreach (var i in intArray)
            {
                sum+=i;
            }
            return sum;
        }        

還有其自有之Split方法。

  static void Main(string[] args)
        {
            //還有其自帶之Split方法
            string str ="Elliot;Mark,Ben.";
            string []strC=str.Split(';',',','.');//刪除人名間之符號
            foreach (var name in strC)
            {
                System.Console.WriteLine(name);
            }
            /*運行結果:
            Elliot
            Mark
            Ben
             */
        }         

 

具名參數

參數之位置不再受約束,而且提高可讀性。

 static void Main(string[] args)
        {
          printf(Age:19,Name:"Elliot");//沒有按照參數列表之順序存入數據
          //運行結果:My name isElliot,I'm 19 years old.
        }           

        static void printf(string Name,int Age)
        {
            System.Console.WriteLine("My name is{0},I'm {1} years old.",Name,Age);
        }

 

可選參數

參數具有默認值。

static void Main(string[] args)
        {
            printf();//不輸入數據,則打印出默認值
            //運行結果:My name isElliot,I'm 19 years old.

        }         
        
        static void printf(string Name="Elliot",int Age=19)//聲明時賦有默認值
        {
            System.Console.WriteLine("My name is{0},I'm {1} years old.",Name,Age);
        }  

 

擴展方法(this參數)

注意:1、方法必須是公有、靜態的,即被public static所修飾;2、必須是形參列表中之第一個,且被this關鍵字所修飾;3、必須由一個靜態類(一般名為SomeTypeExtension)來統一收納對SomeType之擴展方法。

比如,我們想對一個double類型之變量值進行四舍五入,然而其自身並無此方法,必須借助Math中的Round方法。

        static void Main(string[] args)
        {
            double x=3.14159;
            double y=Math.Round(x,4);//只能借助於Math類之Round方法
            System.Console.WriteLine(y);
            //運行結果:3.1416
        }           

所以,我們對double類型之變量加一個擴展方法:

class Program
    {
        static void Main(string[] args)
        {
            double x=3.14159;
            double y=x.Round(4);//這里x就是input,其已自動傳入了
            System.Console.WriteLine(y);
            //運行結果:3.1416
        }           
    }

    public static class DoubleExtension
    {
        public static double Round(this double input,int digital)//參數列表分別對應輸入值(this修飾),保留小數點后幾位
        {
            double result=Math.Round(input,digital);
            return result;
        } 

這樣,就方便甚多。

其擴展方法還與LINQ有很大之聯系,只因余現時所學尚淺,不敢妄議。待日后余學識見漲再加以補充。

 

總結:

傳值參數:參數默認傳遞方式

引用參數:用於需要修改實際參數之值

輸出參數:用於除需返回值外還需其他輸出

數組參數:簡化方法調用

具名參數:提高可讀性,且參數之位置不受約束

可選參數:參數具有默認值

擴展方法(this參數):為目標數據類型“追加”方法

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM