9.1可选参数和命名参数
设计一个方法的参数时,可为部分或全部参数分配默认值。然后,调用这些方法的代码可以选择不指定部分实参,接受其默认值。
除此之外,调用方法时,还可通过指定参数名称的方式为其传递实参。
以下代码演示了可选参数和命名参数的用法:
public static class Program
{
private static Int32 s_n = 0;
private static void M(Int32 x = 9, String s = "A", DateTime dt = default (DateTime), Guid guid = new Guid())
{
Console.WriteLine("x={0},s={1},dt={2},guid={3}", x, s, dt, guid);
}
public static void Main() {
//如果调用时省略了一个实参,C#编译器会自动嵌入参数的默认值
M();
M(8, "x");
//为x显式传递5,指出我想为guid和dt的参数传递一个实参
M(5, guid: Guid.NewGuid(), dt: DateTime.Now);
M(s_n++, s_n++.ToString());
//使用已命名的参数传递实参
M(s: (s_n++).ToString(), x: s_n++);
}
}
Guid(Globally Unique Identifier 全球唯一标识符)一个通过特定算法产生的二进制长度为128位的数字标识符(16 字节),用于指示产品的唯一性。
向方法传递实参时,编译器按从左到右的顺序对实参进行求值。
C#中值传递与引用传递的区别:
把实参当做实参来传递时,就产生了一个新的拷贝。
class Test
{
static void Main(String[] args)
{
int x = 8;
Fo(x);
Console.WriteLine("x={0}", x);
}
static void Fo(int p)
{
p = p + 1;
Console.WriteLine("p={0}", p);
}
}
以上程序运行结果为p=9,x=8; 即X的值不会受P影响,给P赋一个新值并不会改变X的内容,因为P和X存在于内存中不同的位置。
同理,用传值的方式传递一个引用类型对象时,只是复制这个对象本身,即复制其地址值,而不是它指代的对象。下面代码中Fo中看到的StringBuilder对象,就是在Main方法中实例化的那一个,只是有不同的引用指向它而已。
class Test
{
static void Fo(StringBuilder foSB)
{
foSB.Append("test");
foSB = null;
}
static void Main()
{
StringBuilder sb = new StringBuilder();
Fo(sb);
Console.WriteLine(sb.ToString());
}
}
运行结果:test.
换句话说,sb和foSB是指向同一对象的不同引用变量。因为FoSB是引用的拷贝,把它置为null并没有把sb置为 null。
值传递:传的是对象的值拷贝。
引用传递:传的是栈中对象的地址。
ref和out:
ref和out关键字都导致参数通过引用传递。
传递到 ref 形参的实参必须先经过初始化,然后才能传递。
out 形参不同,在传递之前,不需要显式初始化该形参的实参,out形参必须在Method方法中初始化。
关键字相似。
class RefExample
{
static void Method(ref int i)
{
// Rest the mouse pointer over i to verify that it is an int.
// The following statement would cause a compiler error if i
// were boxed as an object.
i = i + 44;
}
static void Main()
{
int val = 1;
Method(ref val);
Console.WriteLine(val);
// Output: 45
}
}
class OutExample
{
static void Method(out int i)
{
i = 44;
}
static void Main()
{
int value;
Method(out value);
// value is now 44
}
}
通过引用传递的效果是,把变量作为参数传递给方法,在方法中修改该参数,会改变这个变量的值。
不要混淆通过引用传递的概念与引用类型的概念。无论方法参数是值类型还是引用类型,均可由 ref 修改。
当通过引用传递时,不会对值类型装箱。
若要使用 ref 参数,方法定义和调用方法均必须显式使用 ref 关键字。
尽管 ref 和 out 关键字会导致不同的运行时行为,它们并不被视为编译时方法签名的一部分。
9.1.1规则和原则
在你定义的方法中,如果为部分参数指定了默认值,请注意下述这些额外的规则和原则:
v 可以为方法,构造器方法和有参属性的参数指定默认值。
v 没有默认值的参数必须放在有默认值的参数之前。例如,如果删除s的默认值(“A“),就会出现编译错误。
v 默认值必须是编译时能确定的常量值。可以设置默认值的参数的类型是:C#认定的基元类型,枚举类型,以及能设为null的任何引用类型。
v 可以用default关键字或new关键字,将值类型的参数的默认值设为值类型的一个实例。
v 不要重命名参数变量(s, x, dt, guid)。否则,任何调用者以传参数名的方式传递实参,都必须修改它们的代码。
v 如果参数用ref或out关键字进行了标识,就不能设置默认值。因为没有办法为这些参数传递有意义的默认值。
v 实参可按任意顺序传递,但命名实参只能出现在实参列表的尾部。
v C#不允许省略逗号之间的实参,比如M(5, , dt: DateTime.Now);
9.1.2 DefaultParameterValueAttribute和OptionalAttribute
在C#中,一旦为某个参数分配了一个默认值,编译器就会在内部向该参数应用一个定制attribute,即System.Runtime.InteropServices.OptionalAttribute。这个attribute会在最终生成的文件的元数据中持久性的存储下来。
9.2 隐式类型的局部变量
针对一个方法中的局部变量,C#允许根据初始化表达式的类型来推断它的类型。
9.3以传引用的方式向方法传递参数
默认情况下,CLR假定所有方法参数都是传值的。传递引用类型的对象时,对一个对象的引用会传给方法。注意这个引用本身是以传值方式传给方法的。
在方法中,必须知道传递的每个参数是引用类型还是值类型,因为用于操纵不同类型的代码可能有显著的差异。
CLR允许以传引用而非传值的方式传递参数。在C#中,这是用关键字out或ref来做到的。这两个关键字都告诉C#编译器生成元数据来指明该参数是传引用的。编译器将生成代码来传递参数的地址,而不是传递参数本身。
如果方法的参数用out来标记,表明不指望调用者在调用方法之前初始化好对象。被调用的方法不能读取参数的值,而且在返回前必须向这个值写入。
public sealed class Program
{
public static void Main()
{
Int32 x;
GetVal(out x);//x在调用GetVal前不必初始化
Console.WriteLine(x);
}
private static void GetVal(out Int32 v)
{
v = 10;//返回前必须初始化v
}
}
在前面的代码中,x是存储在Main的栈帧中声明的,然后x的地址传递给GetVal。GetVal的v是一个指针,它指向Main栈中的Int32值。
栈帧:在执行线程的过程中进行的每个方法调用都会在调用栈中创建并压入一个StackFrame。
相反,如果方法的参数用ref来标记,调用者必须在调用方法前初始化参数的值,被调用的方法可以读取值以及或者向值写入。
9.4向方法传递可变数量的参数
有时候开发人员想定义一个方法来获取可变数量的参数。
public static class Program
{
static Int32 Add(params Int32[] values)
{
Int32 sum = 0;
if (values != null)
{
for (Int32 x = 0; x < values.Length; x++)
{
sum += values[x];
}
}
return sum;
}
public static void Main()
{
//Console.WriteLine(Add(new Int32[] { 1, 2, 3 }));
Console.WriteLine(Add(1, 2, 3));
Console.ReadKey();
}
}
除了params外,以上方法的一切对你来说都应该是非常熟悉的。
很明显数组能用任意数量的一组元素来初始化,再传给Add方法进行处理。
params只能用于方法签名中的最后一个参数。即在方法声明中的 params 关键字之后不允许任何其他参数,并且在方法声明中只允许一个 params 关键字。
params关键字告诉编译器向参数应用System.ParamArrayAttribute的一个实例。
C#编译器检测到一个方法调用时,会先检查所有具有指定名称、同时参数没有应用ParamArrayAttribute的方法。如果找到一个匹配的方法,编译器就生成调用它所需的代码。如果编译器没有找到一个以上匹配的方法,会接着检查应用了ParamArrayAttribute的方法。如果找到一个应用了ParamArrayAttribute的方法,编译器会先生成代码来构造一个数组,填充它的元素,在生成代码来调用选定的方法。
在前一个例子中,没有定义可获取3个Int32兼容实参的Add方法。但是编译器发现在一个Add方法调用中传递了一组Int32值,而且有一个Add方法的Int32数组参数应用了ParamArrayAttribute。因此,编译器会认为这是一个匹配,所以会生成代码,将实参保存到一组Int32数组中,再调用Add方法,并传递该实参。
最终的结果就是你可以直接向Add方法传递一组实参,编译器会生成代码,像上面例子中注释的代码一样,帮你构造和初始化一个数组来容纳实参。
9.5参数和返回类型的指导原则
声明方法的参数类型时,应尽量指定最弱的类型,最好是接口而不是基类。
(重看)
9.6 常量性
CLR没有提供对常量对象/实参的支持。