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参数):为目标数据类型“追加”方法