# Chapter 9 Parameters

# Optional and Named Parameters

When designing a method’s parameters, you can assign default values to some of or all the parameters. Then, code that calls these methods can optionally not specify some of the arguments, thereby accepting the default values. In addition, when you call a method, you can specify arguments by using the name of their parameters. Here is some code that demonstrates using both optional and named parameters.

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() {
 // 1. Same as: M(9, "A", default(DateTime), new Guid());
 M();
 // 2. Same as: M(8, "X", default(DateTime), new Guid());
 M(8, "X");
 // 3. Same as: M(5, "A", DateTime.Now, Guid.NewGuid());
 M(5, guid: Guid.NewGuid(), dt: DateTime.Now);
 // 4. Same as: M(0, "1", default(DateTime), new Guid());
 M(s_n++, s_n++.ToString());
 // 5. Same as: String t1 = "2"; Int32 t2 = 3; 
 // M(t2, t1, default(DateTime), new Guid());
 M(s: (s_n++).ToString(), x: s_n++);
 }
}

When I run this program, I get the following output.

x=9, s=A, dt=1/1/0001 12:00:00 AM, guid=00000000-0000-0000-0000-000000000000
x=8, s=X, dt=1/1/0001 12:00:00 AM, guid=00000000-0000-0000-0000-000000000000
x=5, s=A, dt=8/16/2012 10:14:25 PM, guid=d24a59da-6009-4aae-9295-839155811309
x=0, s=1, dt=1/1/0001 12:00:00 AM, guid=00000000-0000-0000-0000-000000000000
x=3, s=2, dt=1/1/0001 12:00:00 AM, guid=00000000-0000-0000-0000-000000000000

As you can see, whenever arguments are left out at the call site, the C# compiler embeds the parameter’s default value. The third and fifth calls to M use C#’s named parameter feature. In the two calls, I’m explicitly passing a value for x and I’m indicating that I want to pass an argument for the parameters named guid and dt.

When you pass arguments to a method, the compiler evaluates the arguments from left to right. In the fourth call to M, the value in s_n (0) is passed for x, then s_n is incremented, and s_n (1) is passed as a string for s and then s_n is incremented again to 2. When you pass arguments by using named parameters, the compiler still evaluates the arguments from left to right. In the fifth call to M, the value in s_n (2) is converted to a string and saved in a temporary variable (t1) that the compiler creates. Next, s_n is incremented to 3 and this value is saved in another temporary variable (t2) created by the compiler, and then s_n is incremented again to 4. Ultimately, M is invoked, passing it t2, t1, a default DateTime, and a new Guid.

# Rules and Guidelines

There are some additional rules and guidelines that you should know about when defining a method that specifies default values for some of its parameters:

  • You can specify default values for the parameters of methods, constructor methods, and parameterful properties (C# indexers). You can also specify default values for parameters that are part of a delegate definition. Then, when invoking a variable of this delegate type, you can omit the arguments and accept the default values.

  • Parameters with default values must come after any parameters that do not have default values. That is, after you define a parameter as having a default value, then all parameters to the right of it must also have default values. For example, in the definition of my M method, I would get a compiler error if I removed the default value ("A") for s. There is one exception to this rule: a params array parameter (discussed later in this chapter) must come after all parameters (including those that have default values), and the array cannot have a default value itself.

  • Default values must be constant values known at compile time. This means that you can set default values for parameters of types that C# considers to be primitive types, as shown in Table 5-1 in Chapter 5, “Primitive, Reference, and Value Types.” This also includes enumerated types, and any reference type can be set to null. For a parameter of an arbitrary value type, you can set the default value to be an instance of the value type, with all its fields containing zeroes. You can use the default keyword or the new keyword to express this; both syntaxes produce identical Intermediate Language (IL) code. Examples of both syntaxes are used by my M method for setting the default value for the dt parameter and guid parameter, respectively.

  • Be careful not to rename parameter variables because any callers who are passing arguments by parameter name will have to modify their code. For example, in the declaration of my M method, if I rename the dt variable to dateTime, then my third call to M in the earlier code will cause the compiler to produce the following message: error CS1739: The best overload for 'M' does not have a parameter named 'dt'.

  • Be aware that changing a parameter’s default value is potentially dangerous if the method is called from outside the module. A call site embeds the default value into its call. If you later change the parameter’s default value and do not recompile the code containing the call site, then it will call your method passing the old default value. You might want to consider using a default value of 0/null as a sentinel to indicate default behavior; this allows you to change your default without having to recompile all the code with call sites. Here is an example.

// Don’t do this:
private static String MakePath(String filename = "Untitled") {
 return String.Format(@"C:\{0}.txt", filename);
}
// Do this instead:
private static String MakePath(String filename = null) {
 // I am using the null-coalescing operator (??) here; see Chapter 19
 return String.Format(@"C:\{0}.txt", filename ?? "Untitled");
}
  • You cannot set default values for parameters marked with either the ref or out keywords because there is no way to pass a meaningful default value for these parameters.

There are some additional rules and guidelines that you should know about when calling a method by using optional or named parameters:

  • Arguments can be passed in any order; however, named arguments must always appear at the end of the argument list.

  • You can pass arguments by name to parameters that do not have default values, but all required arguments must be passed (by position or by name) for the compiler to compile the code.

  • C# doesn’t allow you to omit arguments between commas, as in M(1, ,DateTime.Now), because this could lead to unreadable comma-counting code. Pass arguments by way of their parameter name if you want to omit some arguments for parameters with default values.

  • To pass an argument by parameter name that requires ref/out, use syntax like the following.

// Method declaration:
private static void M(ref Int32 x) { ... }
// Method invocation:
Int32 a = 5;
M(x: ref a);

💡注意:写 C# 代码和 Microsoft Office 的 COM 对象模型进行互操作性时,C# 的可选参数和命名参数功能非常好用。另外,调用 COM 组件时,如果是以传引用的方式传递实参,C# 还允许省略 ref/out ,进一步简化编码。但如果调用的不是 COM 组件,C# 就要求必须向实参应用 ref/out 关键字。

# The DefaultParameterValue and Optional Attributes

It would be best if this concept of default and optional arguments was not C#-specific. Specifically, we want programmers to define a method indicating which parameters are optional and what their default value should be in a programming language and then give programmers working in other programming languages the ability to call them. For this to work, the compiler of choice must allow the caller to omit some arguments and have a way of determining what those arguments’ default values should be.

In C#, when you give a parameter a default value, the compiler internally applies the System. Runtime.InteropServices.OptionalAttribute custom attribute to the parameter, and this attribute is persisted in the resulting file’s metadata. In addition, the compiler applies System.Runtime.InteropServices.DefaultParameterValueAttribute to the parameter and persists this attribute in the resulting file’s metadata. Then, DefaultParameterValueAttribute ’s constructor is passed the constant value that you specified in your source code.

Now, when a compiler sees that you have code calling a method that is missing some arguments, the compiler can ensure that you’ve omitted optional arguments, grab their default values out of metadata, and embed the values in the call for you automatically.

💡小结:设计方法的参数时,可以给部分或全部参数分配默认值。然后,调用这些方法的代码可以选择不提供部分实参,使用其默认值。此外,调用方法时可通过指定参数名称来传递实参。使用命名参数传递实参时,编译器仍然按从左到右的顺序对实参进行求值。在方法中为部分参数指定默认值需要注意一些规则和原则。可为方法、构造器方法和有参属性(C# 索引器)的参数指定默认值。还可为属于委托定义一部分的参数指定默认值。有默认值的参数必须放在没有默认值的所有参数之后。但这个规则有一个例外:“参数数组” 这种参数必须放在所有参数(包括有默认值的这些)之后,而且数组本身不能有一个默认值。默认值必须是编译时能确定的常量值。这些参数的类型可以是 C# 认定的基元类型、枚举类型,以及能设为 null 的任何引用类型。值类型的参数可将默认值设为值类型的实例,并让它的所有字段都包含零值。可以用 default 关键字或者 new 关键字来表达这个意思,两种语法将生成完全一致的 IL 代码。不要重命名参数变量,否则任何调用者以传参数名的方式传递实参,它们的代码也必须修改。可考虑将默认值 0/null 作为哨兵值使用,从而指出默认行为。这样一来,即使更改了默认值,也不必重新编译包含了 call site(发出调用的地方,可理解成调用了一个目标方法的表达式或代码行,call site 在它的调用中嵌入默认值)的全部代码。如果参数用 ref 或 out 关键字进行了标识,就不能设置默认值。因为没有办法为这些参数传递有意义的默认值。对于可选或命名参数调用来说,实参可按任意顺序传递,但命名实参只能出现在实参列表的尾部。在 C# 中,一旦为参数分配了默认值,编译器就会在内部向该参数应用定制特性 System.Runtime.InteropServices.OptionalAttribute 。该特性会在最终生成的文件的元数据中持久性地储存下来。然后,会向 DefaultParameterValueAttribute 的构造器传递你在源代码中指定的常量值。之后,一旦编译器发现某个方法调用确实了部分实参,就可以确定省略的是可选的实参,并从元数据中提取默认值,将值自动嵌入调用中。

# Implicitly Typed Local Variables

C# supports the ability to infer the type of a method’s local variable from the type of expression that is used to initialize it. The following shows some sample code demonstrating the use of this feature.

private static void ImplicitlyTypedLocalVariables() {
 var name = "Jeff";
 ShowVariableType(name); // Displays: System.String
 // var n = null; // Error: Cannot assign <null> to an implicitly-typed local 
variable
 var x = (String)null; // OK, but not much value
 ShowVariableType(x); // Displays: System.String
 var numbers = new Int32[] { 1, 2, 3, 4 };
 ShowVariableType(numbers); // Displays: System.Int32[]
 // Less typing for complex types
 var collection = new Dictionary<String, Single>() { { "Grant", 4.0f } };
 // Displays: System.Collections.Generic.Dictionary`2[System.String,System.Single]
 ShowVariableType(collection); 
 foreach (var item in collection) {
 // Displays: System.Collections.Generic.KeyValuePair`2[System.String,System.Single]
 ShowVariableType(item);
 }
}
private static void ShowVariableType<T>(T t) {
 Console.WriteLine(typeof(T));
}

The first line of code inside the ImplicitlyTypedLocalVariables method is introducing a new local variable by using the C# var token. To determine the type of the name variable, the compiler looks at the type of the expression on the right side of the assignment operator (=). Because "Jeff" is a string, the compiler infers that name’s type must be String. To prove that the compiler is inferring the type correctly, I wrote the ShowVariableType method. This generic method infers the type of its argument, and then it shows the type that it inferred on the console. I added what ShowVariableType displayed as comments inside the ImplicitlyTypedLocalVariables method for easy reading.

The second assignment (commented out) inside the ImplicitlyTypedLocalVariables method would produce a compiler error (error CS0815: Cannot assign to an implicitlytyped local variable) because null is implicitly castable to any reference type or nullable value type; therefore, the compiler cannot infer a distinct type for it. However, on the third assignment, I show that it is possible to initialize an implicitly typed local variable with null if you explicitly specify a type (String, in my example). Although this is possible, it is not that useful because you could also write String x = null; to get the same result.

In the fourth assignment, you see some real value of using C#’s implicitly typed local variable feature. Without this feature, you’d have to specify Dictionary on both sides of the assignment operator. Not only is this a lot of typing, but if you ever decide to change the collection type or any of the generic parameter types, then you would have to modify your code on both sides of the assignment operator, too.

In the foreach loop, I also use var to have the compiler automatically infer the type of the elements inside the collection. This demonstrates that it is possible and quite useful to use var with foreach, using, and for statements. It can also be useful when experimenting with code. For example, you initialize an implicitly typed local variable from the return type of a method, and as you develop your method, you might decide to change its return type. If you do this, the compiler will automatically figure out that the return type has changed and automatically change the type of the variable! This is great, but of course, other code in the method that uses that variable may no longer compile if the code accesses members by using the variable assuming that it was the old type.

In Microsoft Visual Studio, you can hold the mouse cursor over var in your source code and the editor will display a tooltip showing you the type that the compiler infers from the expression. C#’s implicitly typed local variable feature must be used when working with anonymous types within a method; see Chapter 10, “Properties,” for more details.

You cannot declare a method’s parameter type by using var. The reason for this should be obvious to you because the compiler would have to infer the parameter’s type from the argument being passed at a callsite and there could be no call sites or many call sites. In addition, you cannot declare a type’s field by using var. There are many reasons why C# has this restriction. One reason is that fields can be accessed by several methods and the C# team feels that this contract (the type of the variable) should be stated explicitly. Another reason is that allowing this would permit an anonymous type (discussed in Chapter 10) to leak outside of a single method.

💡重要提示:不要混淆 dynamicvar 。用 var 声明局部变量只有一种简化语法,它要求编译器根据表达式推断具体数据类型。 var 关键字只能声明方法内部的局部变量,而 dynamic 关键字适用于局部变量、字段和参数。表达式不能转型为 var ,但能转型为 dynamic 。必须显式初始化用 var 声明的变量,但无需初始化用 dynamic 声明的变量。欲知 C# dynamic 类型的详情,请参见 5.5 节 “ dynamic 基元类型 "。

💡小结:C# 不能将 null 赋给隐式类型(var 类型)的局部变量。这是由于 null 能隐式转型为任何引用类型或可空值类型。因此,编译器不能推断它的确切类型,除非将 null 强转为具体类型,不过这样做也意义不大。在 VS 中,鼠标放到 var 上将显示一条 “工具提示”,指出编译器根据表达式推断出来的类型。在方法中使用匿名类型时必须用到 C# 的隐式类型局部变量,这会在 “属性” 章节讲解。不能用 var 声明方法的参数类型。原因显而易见,因为编译器必须根据在 call site 传递的实参来推断参数类型,但 call site 可能一个都没有,也可能有好多个。(要么一个类型都推断不出来,要么多个推断发生冲突。)除此之外,不能用 var 声明类型中的字段。

# Passing Parameters by Reference to a Method

By default, the common language runtime (CLR) assumes that all method parameters are passed by value. When reference type objects are passed, the reference (or pointer) to the object is passed (by value) to the method. This means that the method can modify the object and the caller will see the change. For value type instances, a copy of the instance is passed to the method. This means that the method gets its own private copy of the value type and the instance in the caller isn’t affected.

💡重要提示:在方法中,必须知道传递的每个参数是引用类型还是值类型,处理参数的代码显著有别。

The CLR allows you to pass parameters by reference instead of by value. In C#, you do this by using the out and ref keywords. Both keywords tell the C# compiler to emit metadata indicating that this designated parameter is passed by reference, and the compiler uses this to generate code to pass the address of the parameter rather than the parameter itself.

From the CLR’s perspective, out and ref are identical—that is, the same IL is produced regardless of which keyword you use, and the metadata is also identical except for 1 bit, which is used to record whether you specified out or ref when declaring the method. However, the C# compiler treats the two keywords differently, and the difference has to do with which method is responsible for initializing the object being referred to. If a method’s parameter is marked with out, the caller isn’t expected to have initialized the object prior to calling the method. The called method can’t read from the value, and the called method must write to the value before returning. If a method’s parameter is marked with ref, the caller must initialize the parameter’s value prior to calling the method. The called method can read from the value and/or write to the value.

Reference and value types behave very differently with out and ref. Let’s look at using out and ref with value types first.

public sealed class Program { 
 public static void Main() { 
 Int32 x; // x is uninitialized. 
 GetVal(out x); // x doesn’t have to be initialized. 
 Console.WriteLine(x); // Displays "10" 
 } 
 private static void GetVal(out Int32 v) { 
 v = 10; // This method must initialize v. 
 } 
}

In this code, x is declared in Main’s stack frame. The address of x is then passed to GetVal. GetVal’s v is a pointer to the Int32 value in Main’s stack frame. Inside GetVal, the Int32 that v points to is changed to 10. When GetVal returns, Main’s x has a value of 10, and 10 is displayed on the console. Using out with large value types is efficient because it prevents instances of the value type’s fields from being copied when making method calls.

Now let’s look at an example that uses ref instead of out.

public sealed class Program { 
 public static void Main() { 
 Int32 x = 5; // x is initialized. 
 AddVal(ref x); // x must be initialized. 
 Console.WriteLine(x); // Displays "15" 
 } 
 private static void AddVal(ref Int32 v) { 
 v += 10; // This method can use the initialized value in v. 
 } 
}

In this code, x is also declared in Main’s stack frame and is initialized to 5. The address of x is then passed to AddVal. AddVal’s v is a pointer to the Int32 value in Main’s stack frame. Inside AddVal, the Int32 that v points to is required to have a value already. So, AddVal can use the initial value in any expression it desires. AddVal can also change the value, and the new value will be “returned” to the caller. In this example, AddVal adds 10 to the initial value. When AddVal returns, Main’s x will contain 15, which is what gets displayed in the console.

To summarize, from an IL or a CLR perspective, out and ref do exactly the same thing: they both cause a pointer to the instance to be passed. The difference is that the compiler helps ensure that your code is correct. The following code that attempts to pass an uninitialized value to a method expecting a ref parameter produces the following message: error CS0165: Use of unassigned local variable 'x'.

public sealed class Program { 
 public static void Main() { 
 Int32 x; // x is not initialized. 
 // The following line fails to compile, producing 
 // error CS0165: Use of unassigned local variable 'x'. 
 AddVal(ref x); 
 Console.WriteLine(x); 
 } 
 private static void AddVal(ref Int32 v) { 
 v += 10; // This method can use the initialized value in v. 
 } 
}

💡重要提示:经常有人问我,为什么 C# 要求必须在调用方法时指定 outref ?毕竟,编译器知道被调用的方法需要的是 out 还是 ref ,所以应该能正确编译代码。事实上,C# 编译器确实能自动采用正确的操作。但 C# 语言的设计者认为调用者的方法是否需要对传递的变量值进行更改。

另外,CLR 允许根据使用的是 out 还是 ref 参数对方法进行重载。例如,在 C# 中,以下代码是合法的,可以通过编译:

public sealed class Point {
    static void Add(Point p) { ... }
    static void Add(ref Point p) { ... }
}

两个重载方法只有 out 还是 ref 的区别则不合法,因为两个签名的元数据形式完全相同。所以,不能在上述 Point 类型中再定义以下方法:

static void Add(out Point p) { ... }

试图在 Point 类型中添加这个 Add 方法,C# 编译器会显示以下消息: CS0663:"Add" 不能定义仅在ref和out上有差别的重载方法

Using out and ref with value types gives you the same behavior that you already get when passing reference types by value. With value types, out and ref allow a method to manipulate a single value type instance. The caller must allocate the memory for the instance, and the callee manipulates that memory. With reference types, the caller allocates memory for a pointer to a reference object, and the callee manipulates this pointer. Because of this behavior, using out and ref with reference types is useful only when the method is going to “return” a reference to an object that it knows about. The following code demonstrates.

using System; 
using System.IO; 
public sealed class Program { 
 public static void Main() { 
 FileStream fs; // fs is uninitialized 
 // Open the first file to be processed. 
 StartProcessingFiles(out fs); 
 // Continue while there are more files to process. 
 for (; fs != null; ContinueProcessingFiles(ref fs)) { 
 // Process a file. 
 fs.Read(...); 
 } 
 } 
 private static void StartProcessingFiles(out FileStream fs) { 
 fs = new FileStream(...); // fs must be initialized in this method 
 } 
 private static void ContinueProcessingFiles(ref FileStream fs) { 
 fs.Close(); // Close the last file worked on. 
 // Open the next file, or if no more files, "return" null. 
 if (noMoreFilesToProcess) fs = null; 
 else fs = new FileStream (...); 
 } 
}

As you can see, the big difference with this code is that the methods that have out or ref reference type parameters are constructing an object, and the pointer to the new object is returned to the caller. You’ll also notice that the ContinueProcessingFiles method can manipulate the object being passed into it before returning a new object. This is possible because the parameter is marked with the ref keyword. You can simplify the preceding code a bit, as shown here.

using System; 
using System.IO; 
public sealed class Program { 
 public static void Main() { 
 FileStream fs = null; // Initialized to null (required) 
 // Open the first file to be processed. 
 ProcessFiles(ref fs); 
 // Continue while there are more files to process. 
 for (; fs != null; ProcessFiles(ref fs)) { 
 // Process a file. 
 fs.Read(...); 
 } 
 } 
 private static void ProcessFiles(ref FileStream fs) { 
 // Close the previous file if one was open. 
 if (fs != null) fs.Close(); // Close the last file worked on. 
 // Open the next file, or if no more files, "return" null. 
 if (noMoreFilesToProcess) fs = null; 
 else fs = new FileStream (...); 
 } 
}

Here’s another example that demonstrates how to use the ref keyword to implement a method
that swaps two reference types.

public static void Swap(ref Object a, ref Object b) { 
 Object t = b; 
 b = a; 
 a = t; 
}

To swap references to two String objects, you’d probably think that you could write code like the following.

public static void SomeMethod() { 
 String s1 = "Jeffrey"; 
 String s2 = "Richter"; 
 
 Swap(ref s1, ref s2); 
 Console.WriteLine(s1); // Displays "Richter" 
 Console.WriteLine(s2); // Displays "Jeffrey" 
}

However, this code won’t compile. The problem is that variables passed by reference to a method must be of the same type as declared in the method signature. In other words, Swap expects two Object references, not two String references. To swap the two String references, you must do the following.

public static void SomeMethod() { 
 String s1 = "Jeffrey"; 
 String s2 = "Richter"; 
 
 // Variables that are passed by reference 
 // must match what the method expects. 
 Object o1 = s1, o2 = s2; 
 Swap(ref o1, ref o2); 
 // Now cast the objects back to strings. 
 s1 = (String) o1; 
 s2 = (String) o2; 
 
 Console.WriteLine(s1); // Displays "Richter" 
 Console.WriteLine(s2); // Displays "Jeffrey" 
}

This version of SomeMethod does compile and execute as expected. The reason why the parameters passed must match the parameters expected by the method is to ensure that type safety is preserved. The following code, which thankfully won’t compile, shows how type safety could be compromised.

internal sealed class SomeType { 
 public Int32 m_val; 
} 
public sealed class Program { 
 public static void Main() { 
 SomeType st; 
 // The following line generates error CS1503: Argument '1': 
 // cannot convert from 'ref SomeType' to 'ref object'. 
 GetAnObject(out st); 
 Console.WriteLine(st.m_val); 
 } 
 private static void GetAnObject(out Object o) { 
 o = new String('X', 100); 
 } 
}

In this code, Main clearly expects GetAnObject to return a SomeType object. However, because GetAnObject’s signature indicates a reference to an Object, GetAnObject is free to initialize o to an object of any type. In this example, when GetAnObject returned to Main, st would refer to a String, which is clearly not a SomeType object, and the call to Console.WriteLine would certainly fail. Fortunately, the C# compiler won’t compile the preceding code because st is a reference to SomeType, but GetAnObject requires a reference to an Object.

You can use generics to fix these methods so that they work as you’d expect. Here is how to fix the Swap method shown earlier.

public static void Swap<T>(ref T a, ref T b) { 
 T t = b; 
 b = a; 
 a = t; 
}

And now, with Swap rewritten as above, the following code (identical to that shown before) will compile and run perfectly.

public static void SomeMethod() { 
 String s1 = "Jeffrey"; 
 String s2 = "Richter"; 
 Swap(ref s1, ref s2); 
 Console.WriteLine(s1); // Displays "Richter" 
 Console.WriteLine(s2); // Displays "Jeffrey" 
}

For some other examples that use generics to solve this problem, see System.Threading’s Interlocked class with its CompareExchange and Exchange methods.

💡小结:CLR 默认所有方法参数都传值。传递引用类型的对象时,对象引用(或者说指向对象的指针)被传给方法。注意引用(或指针)本身是传值的,意味着方法能修改对象,而调用者能看到这些修改。对于值类型的实例,传给方法的是实例的一个副本,意味着方法将获得它专用的一个值类型实例副本,调用者中的实例不受影响。CLR 允许以传引用而非传值的方式传递参数。C# 用关键字 out 或 ref 支持这个功能。两个关键字都告诉 C# 编译器生成元数据来指明改参数是传引用的。CLR 不区分 out 和 ref,意味着无论用哪个关键字,都会生成相同的 IL 代码。另外,元数据也几乎完全一致,只有一个 bit 除外,它用于记录声明方法时指定的是 out 还是 ref。但是 C# 编译器是区别这两个关键字的。如果方法的参数用 out 标记,表示不指望调用者在调用方法之前初始化好了对象。被调用的方法不能读取参数的值,而且在返回前必须向这个值写入。相反,如果方法的参数用 ref 来标记,调用者就必须在调用该方法前初始化参数的值,被调用的方法可以读取值以及 / 或者向值写入。对于以传引用的方式传给方法的变量,它的类型必须与方法签名中声明的类型相同,这是由于我们无法预测方法内部是否会进行不恰当的转型引起编译错误。

# Passing a Variable Number of Arguments to a Method

It’s sometimes convenient for the developer to define a method that can accept a variable number of arguments. For example, the System.String type offers methods allowing an arbitrary number of strings to be concatenated together and methods allowing the caller to specify a set of strings that are to be formatted together.

To declare a method that accepts a variable number of arguments, you declare the method as follows.

static Int32 Add(params Int32[] values) { 
 // NOTE: it is possible to pass the 'values' 
 // array to other methods if you want to. 
 Int32 sum = 0; 
 if (values != null) {
 for (Int32 x = 0; x < values.Length; x++) 
 sum += values[x]; 
 }
 return sum; 
}

Everything in this method should look very familiar to you except for the params keyword that is applied to the last parameter of the method signature. Ignoring the params keyword for the moment, it’s obvious that this method accepts an array of Int32 values and iterates over the array, adding up all of the values. The resulting sum is returned to the caller.

Obviously, code can call this method as follows.

public static void Main() { 
 // Displays "15" 
 Console.WriteLine(Add(new Int32[] { 1, 2, 3, 4, 5 } )); 
}

It’s clear that the array can easily be initialized with an arbitrary number of elements and then passed off to Add for processing. Although the preceding code would compile and work correctly, it is a little ugly. As developers, we would certainly prefer to have written the call to Add as follows.

public static void Main() { 
 // Displays "15" 
 Console.WriteLine(Add(1, 2, 3, 4, 5)); 
}

You’ll be happy to know that we can do this because of the params keyword. The params keyword tells the compiler to apply an instance of the System.ParamArrayAttribute custom attribute to the parameter.

When the C# compiler detects a call to a method, the compiler checks all of the methods with the specified name, where no parameter has the ParamArray attribute applied. If a method exists that can accept the call, the compiler generates the code necessary to call the method. However, if the compiler can’t find a match, it looks for methods that have a ParamArray attribute to see whether the call can be satisfied. If the compiler finds a match, it emits code that constructs an array and populates its elements before emitting the code that calls the selected method.

In the previous example, no Add method is defined that takes five Int32-compatible arguments; however, the compiler sees that the source code has a call to Add that is being passed a list of Int32 values and that there is an Add method whose array-of-Int32 parameter is marked with the ParamArray attribute. So the compiler considers this a match and generates code that coerces the parameters into an Int32 array and then calls the Add method. The end result is that you can write the code, easily passing a bunch of parameters to Add, but the compiler generates code as though you’d written the first version that explicitly constructs and initializes the array.

Only the last parameter to a method can be marked with the params keyword (ParamArrayAttribute). This parameter must also identify a single-dimension array of any type. It’s legal to pass null or a reference to an array of 0 entries as the last parameter to the method. The following call to Add compiles fine, runs fine, and produces a resulting sum of 0 (as expected).

public static void Main() { 
 // Both of these lines display "0" 
 Console.WriteLine(Add()); // passes new Int32[0] to Add
 Console.WriteLine(Add(null)); // passes null to Add: more efficient (no array allocated)
}

So far, all of the examples have shown how to write a method that takes an arbitrary number of Int32 parameters. How would you write a method that takes an arbitrary number of parameters where the parameters could be any type? The answer is very simple: just modify the method’s prototype so that it takes an Object[] instead of an Int32[]. Here’s a method that displays the Type of every object passed to it.

public sealed class Program { 
 public static void Main() { 
 DisplayTypes(new Object(), new Random(), "Jeff", 5); 
 } 
 private static void DisplayTypes(params Object[] objects) { 
 if (objects != null) {
 foreach (Object o in objects) 
 Console.WriteLine(o.GetType()); 
 }
 } 
}

Running this code yields the following output.

System.Object
System.Random
System.String
System.Int32

💡重要提示:注意,调用参数数量可变的方法对性能有所影响 (除非显式传递 null )。毕竟,数组对象必须在堆上分配,数组元素必须初始化,而且数组的内存最终需要垃圾回收。要减少对性能的影响,可考虑定义几个没有使用 params 关键字的重载版本。关于这方面的范例,请参考 System.String 类的 Concat 方法,该方法定义了以下重载版本:

public sealed class String : Object, ... {
    public static string Concat(object arg0);
    public static string Concat(object arg0, object arg1);
    public static string Concat(object arg0, object arg1, object arg2);
    public static string Concat(params object[] args);
    public static string Concat(string str0, string str1);
    public static string Concat(string str0, string str1, string str2);
    public static string Concat(string str0, string str1, string str2, string str3);
    public static string Concat(params string[] values);
}

如你所见, Concat 方法定义了几个没有使用 params 关键字的重载版本。这是为了改善常规情形下的性能。使用了 params 关键字的重载则用于不太常见的情形;在这些情形下,性能有一定的损失。但幸运的是,这些情形本来就不常见。

💡小结:方法有时需要获取可变数量的参数。为了接受可变数量的参数,要在原先的参数定义前面加上 params 关键字, params 只能应用于方法签名中的最后一个参数。 params 关键字告诉编译器向参数应用定制特性 System.ParamArrayAttribute 的一个实例。C# 编译器检测到方法调用时,会先检查所有具有指定名称、同时参数没有应用 ParamArray 特性的方法。找到匹配的方法,就生成调用它所需的代码。没有找到,就接着检查应用了 ParamArray 特性的方法。找到匹配的方法,编译器先生成代码来构造一个数组,填充它的元素,再生成代码来调用所选的方法。只有方法的最后一个参数才可以用 params 关键字( ParamArrayAttribute )标记。另外,这个参数只能标记一维数组(任意类型)。可为这个参数传递 null 值,或传递对包含零个元素的一个数组的引用。

# Parameter and Return Type Guidelines

When declaring a method’s parameter types, you should specify the weakest type possible, preferring interfaces over base classes. For example, if you are writing a method that manipulates a collection of items, it would be best to declare the method’s parameter by using an interface such as IEnumerable rather than using a strong data type such as List or even a stronger interface type such as ICollection or IList .

// Desired: This method uses a weak parameter type 
public void ManipulateItems<T>(IEnumerable<T> collection) { ... } 
// Undesired: This method uses a strong parameter type 
public void ManipulateItems<T>(List<T> collection) { ... }

The reason, of course, is that someone can call the first method passing in an array object, a List object, a String object, and so on—any object whose type implements IEnumerable . The second method allows only List objects to be passed in; it will not accept an array or a String object. Obviously, the first method is better because it is much more flexible and can be used in a much wider range of scenarios.

Naturally, if you are writing a method that requires a list (not just any enumerable object), then you should declare the parameter type as an IList . You should still avoid declaring the parameter type as List. Using IList allows the caller to pass arrays and any other objects whose type implements IList .

Note that my examples talked about collections, which are designed using an interface architecture. If we were talking about classes designed using a base class architecture, the concept still applies. So, for example, if I were implementing a method that processed bytes from a stream, we’d have the following.

// Desired: This method uses a weak parameter type 
public void ProcessBytes(Stream someStream) { ... } 
// Undesired: This method uses a strong parameter type 
public void ProcessBytes(FileStream fileStream) { ... }

The first method can process bytes from any kind of stream: a FileStream , a NetworkStream , a MemoryStream , and so on. The second method can operate only on a FileStream , making it far more limited.

On the flip side, it is usually best to declare a method’s return type by using the strongest type possible (trying not to commit yourself to a specific type). For example, it is better to declare a method that returns a FileStream object as opposed to returning a Stream object.

// Desired: This method uses a strong return type 
public FileStream OpenFile() { ... } 
// Undesired: This method uses a weak return type 
public Stream OpenFile() { ... }

Here, the first method is preferred because it allows the method’s caller the option of treating the returned object as either a FileStream object or as a Stream object. Meanwhile, the second method requires that the caller treat the returned object as a Stream object. Basically, it is best to let the caller have as much flexibility as possible when calling a method, allowing the method to be used in the widest range of scenarios.

Sometimes you want to retain the ability to change the internal implementation of a method without affecting the callers. In the example just shown, the OpenFile method is unlikely to ever change its internal implementation to return anything other than a FileStream object (or an object whose type is derived from FileStream ). However, if you have a method that returns a List object, you might very well want to change the internal implementation of this method in the future so that it would instead return a String[]. In the cases in which you want to leave yourself some flexibility to change what your method returns, choose a weaker return type. The following is an example.

// Flexible: This method uses a weaker return type 
public IList<String> GetStringCollection() { ... } 
// Inflexible: This method uses a stronger return type 
public List<String> GetStringCollection() { ... }

In this example, even though the GetStringCollection method uses a List object internally and returns it, it is better to prototype the method as returning an IList instead. In the future, the GetStringCollection method could change its internal collection to use a String[], and callers of the method won’t be required to change any of their source code. In fact, they won’t even have to recompile their code. Notice in this example that I’m using the strongest of the weakest types. For instance, I’m not using an IEnumerable or even ICollection .

💡小结:声明方法的参数类型时,应尽量指定最弱的类型,宁愿要接口也不要基类。原因是这样能使方法更灵活,适合更广泛的情形。除非方法需要的类型有特定的要求。不仅是接口体系结构实用上面的概念,对于基类体系结构设计的类时,概念同样适用。相反,一般最好是将方法的返回类型声明为最强的类型(防止受限于特定类型)。如果想保持一定的灵活性,在将来更改方法返回的东西,那么就选择一个较弱的(注意是相较于最强的较弱)返回返回类型。

# Const-ness

n some languages, such as unmanaged C++, it is possible to declare methods or parameters as a constant that forbids the code in an instance method from changing any of the object’s fields or prevents the code from modifying any of the objects passed into the method. The CLR does not provide for this, and many programmers have been lamenting this missing feature. Because the CLR doesn’t offer this feature, no language (including C#) can offer this feature.

First, you should note that in unmanaged C++, marking an instance method or parameter as const ensured only that the programmer could not write normal code that would modify the object or parameter. Inside the method, it was always possible to write code that could mutate the object/ parameter by either casting away the const-ness or by getting the address of the object/argument and then writing to the address. In a sense, unmanaged C++ lied to programmers, making them believe that their constant objects/arguments couldn’t be written to even though they could.

When designing a type’s implementation, the developer can just avoid writing code that manipulates the object/arguments. For example, strings are immutable because the String class doesn’t offer any methods that can change a string object.

Also, it would be very difficult for Microsoft to endow the CLR with the ability to verify that a constant object/argument isn’t being mutated. The CLR would have to verify at each write that the write was not occurring to a constant object, and this would hurt performance significantly. Of course, a detected violation would result in the CLR throwing an exception. Furthermore, constant support adds a lot of complexity for developers. For example, if a type is immutable, all derived types would have to respect this. In addition, an immutable type would probably have to consist of fields that are also of immutable types.

These are just some of the reasons why the CLR does not support constant objects/arguments.

💡小结:有的语言(比如非托管 C++)允许将方法或参数声明为常量,从而禁止实例方法中的代码更改对象的任何字段,或者更改传给方法的任何对象。CLR 没有提供这个功能,既然 CLR 都不提供,那么面向它的任何编程语言(包括 C#)自然也无法提供。不过非托管 C++ 将实例方法或参数声明为 const 只能防止程序员用一般的代码来更改对象或参数。方法内部总是可以更改对象或实参的。例如用强制类型转换去掉 “常量性”,或者通过获取对象 / 实参的地址,在向那个地址写入。实现类型时,开发人员可以避免写操纵对象或实参的代码。例如,String 类就没有提供任何能更改 String 对象的方法,所以字符串是不可变(immutable)的。此外,Microsoft 很难为 CLR 赋予验证常量对象 / 实参未被更改的能力。CLR 将不得不对每个写入操作进行验证,确定该写入针对的不是常量对象。这对性能影响很大。当然,如果检测到有违反常量性的地方,会造成 CLR 抛出异常。此外,如果支持常量性,还会给开发人员带来大量复杂性。例如,如果类型是不可变的,它的所有派生类型都不得不遵守这个约定。除此之外,在不可变的类型中,字段也必须不可变。考虑到种种原因,CLR 没有提供对常量对象 / 实参的支持。