C#本质论笔记 第4章 方法和参数

方法

方法是包含一系列语句的代码块。 程序通过调用该方法并指定任何所需的方法参数使语句得以执行。 在 C# 中,每个执行的指令均在方法的上下文中执行。 Main 方法是每个 C# 应用程序的入口点,并在启动程序时由公共语言运行时 (CLR) 调用。

规范

要为方法名使用动词或动词短语。

1
System.Console.Write("Enter Your first name: ")

解析

  • System 命名空间
  • Console 类型名称
  • Write 方法名称
  • "Enter Your first name: " 实参

命名空间

命名空间是一种分类机制,用于组合功能相关的所有类型。System 命名空间包含了用于执行大量基本变成活动的类型。

常见命名空间

命名空间 描述
System 主命名空间
System.Collections.Generic 泛型集合接口和类
System.Collections 使用Arraylist
System.Linq 语言集成查询
System.Text 编码方式
System.Text.RegularExpressions 正则表达式
System.Threading 多线程
System.Windows.Forms winform窗体
System.Windows.Forms.Control winform控件
System.IO 操作文件、文件夹
System.Data 操作ado.net
System.Data.SqlClient 操作ado.net
System.Net 操作网络
System.Net.Sockets 操作网络套接字
System.Management 获取硬件信息(需要添加引用)
System.Drawing 绘制系统
System.Xml 操作xml
System.Media 播放wav和系统wav文件
Microsoft.Win32 操作注册表
System.Runtime.InteropServices 引用dll
System.Security.Cryptography 加密解密
System.Text.RegularExpressions 正则表达式
Microsoft.VisualBasic 简繁体转换(需要添加引用)
System.Diagnostics 调试输出
System.Web.UI web页面
System.Web.UI.Control web控件
System.Configuration 配置信息
System.DateTime 操作时间
System.Math 操作数字类
Microsoft.Win32.Registry 操作注册表

方法的声明和调用

在类或者结构中声明方法。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class IntroducingMethods {
static void Main (string[] args) {
string firstName;
string lastName;
string fullName;

System.Console.WriteLine ("Hey you!");

firstName = GetUserInput ("Enter your first name: ");
lastName = GetUserInput ("Enter you last name: ");

fullName = GetFullName (firstName, lastName);

DisplayGreeting (fullName);
}

static string GetUserInput (string prompt) {
System.Console.Write (prompt);
return System.Console.ReadLine ();
}

static string GetFullName (string firstName, string lastName) {
return firstName + " " + lastName;
}

static void DisplayGreeting (string name) {
System.Console.WriteLine ("Your full name is {0}.", name);
return;
}
}

初学者主题:用方法进行重构

将一组相关语句转移到一个方法中,而不是把它们留在一个较大的方法中,这是重构 refactoring 的一种形式。

  • 重构有助于减少重复代码,因为可以从多个位置调用方法,而不必在每个位置都重复这个方法的代码。
  • 重构还有助于增强代码的可读性。

方法的返回类型和返回值

方法可以将值返回给调用方。 如果列在方法名之前的返回类型 不是 void,则该方法可通过使用 return 关键字返回值。 带 return 关键字,后跟与返回类型匹配的值的语句将该值返回到方法调用方。

return 语句并非只能在方法末尾出现,例如,ifswitch 语句中可以包含 return 语句,举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Program
{
static bool MyMethod()
{
string command = ObtainCommand();
switch(command)
{
case "quit":
return false;
// ... omitted, other cases
default:
return true;
}
}
}

如果 return 之后有“不可达”的语句,编译器会发出警告,指出有永远执行不到的语句。

指定 void 作为返回类型,表示没有返回值,所以,return 可有可无

using 指令

C# 允许简写类的全名,为此,要在文件的顶部列出类的命名空间(名称空间),前面加上 using 关键字。在文件的其他地方,就可以使用其类型名称来引用空间中的类型了。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
// The using directive imports all types from the
// specified namespace into the entire file.
using System;

class HelloWorld
{
static void Main()
{
// No need to qualify Console with System
// because of the using directive above.
Console.WriteLine("Hello, my name is Inigo Montoya");
}
}

如果要引用某个命名空间的子命名空间中的类型,同样需要在文件顶部用 using 语句明确指定子命名空间引用。例如,要访问 System.Text 中的 StringBuilder 类型,必须增加一个 using System.Text; 指令,或者使用 Syste.Text.StringBuilder 对类型进行完全限定,而不能仅仅写成 Text.StringBuilder

使用别名

可以利用 using 指令为命名空间或类型取一个别名。别名alias是在using指令起作用范围内可以使用的替代名称。别名的两个最常见的用途是消除两个同名类型的歧义和缩写长名称。

例如,System.Timers 中存在 Timer 类型,System.Threading 中也存在一个 Timer 类型,当这两个命名空间同时存在并同时需要被引用时,需要用别名来加以区分:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Threading;
using CountDownTimer = System.Timers.Timer;

class HelloWorld
{
static void Main()
{
CountDownTimer timer;
// ...
}
}

如果别名跟要区别的类型名相同,或者说别名占用了类型名的名字,那么如果要引用另外一个类型,就必须完全限定或者定义新的不同的别名,如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Threading;

// Declare alias Timer to refer to System.Timers.Timer to
// avoid code ambiguity with System.Threading.Timer
using Timer = System.Timers.Timer;

class HelloWorld
{
static void Main()
{
Timer timer;
// ...
}
}

上例中,Timer 时别名,如果要引用 System.Threading.Timer类型,必须完全限定或者定义不同的别名。这又是要搞么?😥

高级主题:嵌套的 using 指令

也可以在命名空间内部使用 using 指令,有效范围限于命名空间内部。不过,很少这样使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace EssentialCSharp
{
using System;

class HelloWorld
{
static void Main()
{
// No need to qualify Console with System
// because of the using directive above.
Console.WriteLine("Hello, my name is Inigo Montoya");
}
}
}

Main() 和命令行参数

Main 方法是 C# 应用程序的入口点。 (库和服务不要求使用 Main 方法作为入口点)。Main 方法是应用程序启动后调用的第一个方法。

C# 程序中只能有一个入口点。 如果多个类包含 Main 方法,必须使用 /main 编译器选项来编译程序,以指定将哪个 Main 方法用作入口点。 有关详细信息,请参阅 /main(C# 编译器选项)。

概述

  • Main 方法是可执行程序的入口点,也是程序控制开始和结束的位置。
  • Main 在类或结构中声明。 Main 必须是静态方法,不得为公共方法。 (在前面的示例中,它获得的是私有成员的默认访问权限)。封闭类或结构不一定要是静态的。
  • Main 可以具有 void、int,或者以 C# 7.1、Task 或 Task 返回类型开头。
  • 当且仅当 Main 返回 Task 或 Task 时,Main 的声明可包括 async 修饰符。 请注意,该操作可明确排除 async void Main 方法。
  • 使用或不使用包含命令行自变量的 string[] 参数声明 Main 方法都行。 使用 Visual Studio 创建 Windows 应用程序时,可以手动添加此形参,也可以使用 Environment 类来获取命令行实参。 参数被读取为从零开始编制索引的命令行自变量。 与 C 和 C++ 不同,程序的名称不被视为第一个命令行自变量。

命令行参数

可以通过以下方式之一定义方法来将自变量发送到 Main 方法:

1
static int Main(string[] args)
1
static void Main(string[] args)

程序运行时,通过 string 数组参数将命令行参数传递给 Main()。要获取参数,操作数组就可以了。

示例

命令行输入 传递给 Main 的字符串数组
executable.exe a b c “a”
“b”
“c”
executable.exe one two “one”
“two”
executable.exe “one two” three “one two”
“three”

实例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CommandLine {
static void Main (string[] args) {
// The Length property provides the number of array elements
System.Console.WriteLine ("parameter count = {0}", args.Length);

for (int i = 0; i < args.Length; i++) {
System.Console.WriteLine ("Arg[{0}] = [{1}]", i, args[i]);
}
}
}
/* 执行 CommandLine.exe a b c 输出:
parameter count = 3
Arg[0] = [a]
Arg[1] = [b]
Arg[2] = [c]
*/
/* 执行 commandline.exe "a b" c 输出:
parameter count = 2
Arg[0] = [a b]
Arg[1] = [c]
*/

高级主题:多个 Main() 方法

如果多个类包含 Main 方法,对 csc.exe 使用 /main:class 选项将指定包含程序入口点的类。

示例

编译 t2.cs 和 t3.cs,指出 Main 方法可在 Test2 中找到:

csc t2.cs t3.cs /main:Test2  

方法的参数

按引用传递和按值传递参数

默认情况下,值类型传递给方法时,传递的是副本而不是对象本身。 因此,对参数的更改不会影响调用方法中的原始副本。 可以使用 ref 关键字按引用传递值类型。 有关详细信息,请参阅传递值类型参数。 有关内置值类型的列表,请参阅值类型表

引用类型的对象传递到方法中时,将传递对对象的引用。 也就是说,该方法接收的不是对象本身,而是指示该对象位置的参数。 如果通过使用此引用更改对象的成员,即使是按值传递该对象,此更改也会反映在调用方法的参数中。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
public class SampleRefType {
public int value;
}
public class Test {
public static void Main () {
SampleRefType rt = new SampleRefType ();
rt.value = 11;
ModifyObject (rt);
Console.WriteLine (rt.value);
}
static void ModifyObject (SampleRefType obj) {
obj.value = 66;
}
}
/* 输出
66
*/

在 C# 中,实参可以按值或按引用传递给形参。 按引用传递使函数成员、方法、属性、索引器、运算符和构造函数可以更改参数的值,并让该更改在调用环境中保持。 若要按引用传递参数,请使用 refout 关键字。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
class Program {
static void Main (string[] args) {
int arg;

// Passing by value.
// The value of arg in Main is not changed.
arg = 4;
squareVal (arg);
Console.WriteLine (arg);
// Output: 4

// Passing by reference.
// The value of arg in Main is changed.
arg = 4;
squareRef (ref arg);
Console.WriteLine (arg);
// Output: 16
}

static void squareVal (int valParameter) {
valParameter *= valParameter;
}

// Passing by reference
static void squareRef (ref int refParameter) {
refParameter *= refParameter;
}
}

引用参数 ref

在方法的参数列表中使用 ref 关键字时,它指示参数按引用传递,而非按值传递。 按引用传递的效果是,对所调用方法中参数进行的任何更改都反映在调用方法中。 例如,如果调用方传递本地变量表达式或数组元素访问表达式,所调用方法会替换 ref 参数引用的对象,然后,当该方法返回时,调用方的本地变量或数组元素将开始引用新对象。

若要使用 ref 参数,方法定义和调用方法均必须显式使用 ref 关键字,如下面的示例所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;

class RefExample {
static void Method (ref int i) {
i = i + 44;
}

static void Main () {
int val = 1;
Method (ref val);
Console.WriteLine (val); // Output: 45
}
}

输出参数 out

out 关键字通过引用传递参数。 它与 ref 关键字相似,只不过 ref 要求在传递之前初始化变量。 若要使用 out 参数,方法定义和调用方法均必须显式使用 out 关键字。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;

class OutExample {
static void Method (out int i) {
i = 44;
}

static void Main () {
int value;
Method (out value);
Console.WriteLine (value); // value is now 44
}
}

调用具有 out 参数的方法

C# 6 及更早版本中,必须先在单独的语句中声明变量,然后才能将其作为 out 参数传递。 下面的示例先声明了变量 number,然后再将它传递给将字符串转换为数字的 Int32.TryParse 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

public class Example {
public static void Main () {
string value = "1640";

int number;
if (Int32.TryParse (value, out number))
Console.WriteLine ($"Converted '{value}' to {number}");
else
Console.WriteLine ($"Unable to convert '{value}'");
}
}
// The example displays the following output:
// Converted '1640' to 1640

C# 7 开始,可以在方法调用的参数列表而不是单独的变量声明中声明 out 变量。 这使得代码更简洁可读,还能防止在方法调用之前无意中向该变量赋值。 下面的示例与上一个示例基本相同,不同之处在于它在对 Int32.TryParse 方法的调用中定义了 number 变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

public class Example {
public static void Main () {
string value = "1640";

if (Int32.TryParse (value, out int number))
Console.WriteLine ($"Converted '{value}' to {number}");
else
Console.WriteLine ($"Unable to convert '{value}'");
}
}
// The example displays the following output:
// Converted '1640' to 1640

数组参数

参数以数组形式体现,即数组参数。数组可以作为实参传递给方法形参。由于数组是引用类型,因此方法可以更改元素的值。

将一维数组作为参数传递

  • 可将初始化的一维数组传递给方法。 例如,下列语句将一个数组发送给了 Print 方法。
1
2
int[] theArray = { 1, 3, 5, 7, 9 };
PrintArray(theArray);
  • 可在同一步骤中初始化并传递新数组,如下例所示。
1
PrintArray(new int[] { 1, 3, 5, 7, 9 });

将多维数组作为参数传递

  • 通过与传递一维数组相同的方式,向方法传递初始化的多维数组。
1
2
int[,] theArray = { { 1, 2 }, { 2, 3 }, { 3, 4 } };
Print2DArray(theArray);
  • 可在同一步骤中初始化并传递新数组,如下例所示。
1
Print2DArray(new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } });

示例

在下列示例中,初始化一个整数的二维数组,并将其传递至 Print2DArray 方法。 该方法将显示数组的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ArrayClass2D {
static void Print2DArray (int[, ] arr) {
// Display the array elements.
for (int i = 0; i < arr.GetLength (0); i++) {
for (int j = 0; j < arr.GetLength (1); j++) {
System.Console.WriteLine ("Element({0},{1})={2}", i, j, arr[i, j]);
}
}
}
static void Main () {
// Pass the array as an argument.
Print2DArray (new int[, ] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } });

// Keep the console window open in debug mode.
System.Console.WriteLine ("Press any key to exit.");
System.Console.ReadKey ();
}
}
/* Output:
Element(0,0)=1
Element(0,1)=2
Element(1,0)=3
Element(1,1)=4
Element(2,0)=5
Element(2,1)=6
Element(3,0)=7
Element(3,1)=8
*/

示例

传递一个长度可变的参数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.IO;

class PathEx {
static void Main () {
string fullName;

// Call Combine() with four arguments
fullName = Combine (
Directory.GetCurrentDirectory (),
"bin", "config", "index.html");

Console.WriteLine (fullName);

// Call Combine() with only three arguments
fullName = Combine (
Environment.SystemDirectory,
"Temp", "index.html");

Console.WriteLine (fullName);

// Call Combine() with an array
fullName = Combine (
new string[] {
"C:\\",
"Data",
"HomeDir",
"index.html"
});

Console.WriteLine (fullName);
// ...
}

static string Combine (params string[] paths) {
string result = string.Empty;
foreach (string path in paths) {
result = System.IO.Path.Combine (result, path);
}
return result;
}
}
/* 输出
d:\WaProj\CSharpGuide\bin\config\index.html
C:\Windows\system32\Temp\index.html
C:\Data\HomeDir\index.html
*/

数组参数需要注意的地方

  • 参数不一定是方法的唯一参数,但必须时方法声明中的最后一个参数。所以,只能有一个数组参数;
  • 调用者可以显式地使用数组,而不必是以都好分隔的参数列表。最终生成的CIL代码是一样的。

递归

递归是一项非常重要的编程技巧,它使函数调用其本身。

示例

返回目录中所有 .cs文件代码行总数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using System;
using System.IO;

public static class LineCounter {
// Use the first argument as the directory
// to search, or default to the current directory.
public static void Main (string[] args) {
int totalLineCount = 0;
string directory;
if (args.Length > 0) {
directory = args[0];
} else {
directory = Directory.GetCurrentDirectory ();
}
totalLineCount = DirectoryCountLines (directory);
System.Console.WriteLine (totalLineCount);
}

static int DirectoryCountLines (string directory) {
int lineCount = 0;
foreach (string file in
Directory.GetFiles (directory, "*.cs")) {
lineCount += CountLines (file);
}

foreach (string subdirectory in
Directory.GetDirectories (directory)) {
lineCount += DirectoryCountLines (subdirectory);

}

return lineCount;
}
private static int CountLines (string file) {
string line;
int lineCount = 0;
FileStream stream =
new FileStream (file, FileMode.Open);

StreamReader reader = new StreamReader (stream);
line = reader.ReadLine ();

while (line != null) {
if (line.Trim () != "") {
lineCount++;
}
line = reader.ReadLine ();
}

reader.Close (); // Automatically closes the stream
return lineCount;
}
}

示例

遍历目录中所有目录和文件,在终端中格式化输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System;
using System.IO;

namespace DirectoryFileList {
class Program {
static void Main (string[] args) {
string directory;
if (args.Length > 0) {
directory = args[0];
} else {
directory = Directory.GetCurrentDirectory ();
}

Console.WriteLine (directory);

DirectoryFileList (directory, directory);

Console.ReadKey ();
}

static void DirectoryFileList (string beginDirectory, string currentDirectory) {
int level = currentDirectory.Split ('\\').Length - beginDirectory.Split ('\\').Length;

string formatPrefix = "|--";

for (int currentLevel = 0; currentLevel < level; currentLevel++) {
formatPrefix += "|--";
}

foreach (string subdirectory in Directory.GetDirectories (currentDirectory)) {
Console.WriteLine (formatPrefix + subdirectory.Substring (subdirectory.LastIndexOf ('\\')));

DirectoryFileList (beginDirectory, subdirectory);
}

foreach (string file in Directory.GetFiles (currentDirectory)) {
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine (formatPrefix + Path.GetFileName (file));
Console.ForegroundColor = ConsoleColor.White;
}
}
}
}
/* 输出
d:\WaProj\CSharpGuide
|--\.vscode
|--|--launch.json
|--|--tasks.json
|--\bin
|--|--\Debug
|--|--|--\netcoreapp2.0
|--|--|--|--CSharpGuide.deps.json
|--|--|--|--CSharpGuide.dll
|--|--|--|--CSharpGuide.pdb
|--|--|--|--CSharpGuide.runtimeconfig.dev.json
|--|--|--|--CSharpGuide.runtimeconfig.json
|--\obj
|--|--\Debug
|--|--|--\netcoreapp2.0
|--|--|--|--CSharpGuide.AssemblyInfo.cs
|--|--|--|--CSharpGuide.AssemblyInfoInputs.cache
|--|--|--|--CSharpGuide.csproj.CoreCompileInputs.cache
|--|--|--|--CSharpGuide.csproj.FileListAbsolute.txt
|--|--|--|--CSharpGuide.dll
|--|--|--|--CSharpGuide.pdb
|--|--CSharpGuide.csproj.nuget.cache
|--|--CSharpGuide.csproj.nuget.g.props
|--|--CSharpGuide.csproj.nuget.g.targets
|--|--project.assets.json
|--CSharpGuide.csproj
|--Program.cs
*/

递归很容易陷入无限递归调用,编码/调试时多注意。

方法重载 Mathod Overloading

一个类包含两个或者更多同名的方法,就会发生方法重载。以WriteLine()方法为例,可向它传递一个格式字符串和其他一些参数,也可以只传递一个整数。两者的实现肯定不同,但在逻辑上,对于调用者,这个方法就是负责输出数据,至于方法内部如恶化实现,调用者并不关心。

使用重载统计.cs文件中的行数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System.IO;

public static class LineCounter {
public static void Main (string[] args) {
int totalLineCount;

if (args.Length > 1) {
totalLineCount =
DirectoryCountLines (args[0], args[1]);

}
if (args.Length > 0) {
totalLineCount = DirectoryCountLines (args[0]);

} else {
totalLineCount = DirectoryCountLines ();
}

System.Console.WriteLine (totalLineCount);
}

static int DirectoryCountLines ()

{
return DirectoryCountLines (
Directory.GetCurrentDirectory ());
}

static int DirectoryCountLines (string directory)

{
return DirectoryCountLines (directory, "*.cs");
}

static int DirectoryCountLines (

string directory, string extension) {
int lineCount = 0;
foreach (string file in
Directory.GetFiles (directory, extension)) {
lineCount += CountLines (file);
}

foreach (string subdirectory in
Directory.GetDirectories (directory)) {
lineCount += DirectoryCountLines (subdirectory);
}

return lineCount;
}

private static int CountLines (string file) {
int lineCount = 0;
string line;
FileStream stream =
new FileStream (file, FileMode.Open);

StreamReader reader = new StreamReader (stream);
line = reader.ReadLine ();
while (line != null) {
if (line.Trim () != "") {
lineCount++;
}
line = reader.ReadLine ();
}

reader.Close (); // Automatically closes the stream
return lineCount;
}
}

可选参数(可选实参)

方法、构造函数、索引器或委托的定义可以指定其形参为必需还是可选。 任何调用都必须为所有必需的形参提供实参,但可以为可选的形参省略实参。

每个可选形参都有一个默认值作为其定义的一部分。 如果没有为该形参发送实参,则使用默认值。 默认值必须是以下类型的表达式之一:

  • 常量表达式;
  • new ValType() 形式的表达式,其中 ValType 是值类型,例如 enum 或 struct;
  • default(ValType) 形式的表达式,其中 ValType 是值类型。

可选参数定义于参数列表的末尾和必需参数之后。 如果调用方为一系列可选形参中的任意一个形参提供了实参,则它必须为前面的所有可选形参提供实参。例如,在以下代码中,使用一个必选形参和两个可选形参定义实例方法 ExampleMethod

1
public void ExampleMethod(int required, string optionalstr = "default string", int optionalint = 10)

下面对 ExampleMethod 的调用会导致编译器错误,原因是为第三个形参而不是为第二个形参提供了实参。

//anExample.ExampleMethod(3, ,4);

但是,如果知道第三个形参的名称,则可以使用命名实参来完成此任务。

anExample.ExampleMethod(3, optionalint: 4);

命名参数(命名实参)

命名参数C# 4.0 新增的方法调用功能。有了命名实参,你将不再需要记住或查找形参在所调用方法的形参列表中的顺序。 每个实参的形参都可按形参名称进行指定。

如果你不记得参数的顺序,但知道其名称,你可以按任何顺序发送自变量。

PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");
PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);

高级主题:方法解析

当编译器必须从一系列“适用”的方法中训责一个最适合某个特定调用的方法时,会选择拥有最据提的参数类型的那个方法。

如果有多个适用的方法,但无法从中挑选出最具唯一性的,编译器就会宝座,指明调用存在的歧义。

例如,给定以下的方法:

1
2
3
4
static void Method(object thing){}
static void Method(double thing){}
static void Method(long thing){}
static void Method(int thing){}

调用 Method(42) 会被解析成 Method(int thing),因为存在着一个从实参类型到形参类型的完全匹配的方法。如果删除该方法,那么重载解析会选择 long 版本,因为 longdoubleobject 更具体。

异常处理语句

C# 提供用于处理反常情况(称为异常,可能会在执行程序的过程中发生)的内置支持。 这些异常由正常控制流之外的代码进行处理。

try-catch

Try 块之后必须紧跟着一个或多个 catch 块(或/和一个 Finally 块),这些子句指定不同异常的处理程序。只要数据类型与异常类型匹配,对应的 catch 块就会执行。

在前面例子“遍历目录中所有目录和文件,在终端中格式化输出”中,如果权限不够,就会引发异常,下面例子加入异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;
using System.IO;

namespace DirectoryFileList {
class Program {
static void Main (string[] args) {
string directory;
if (args.Length > 0) {
directory = args[0];
} else {
directory = Directory.GetCurrentDirectory ();
}

Console.WriteLine (directory);
DirectoryFileList (directory, directory);
Console.ReadKey ();
}

static void DirectoryFileList (string beginDirectory, string currentDirectory) {
int level = currentDirectory.Split ('\\').Length - beginDirectory.Split ('\\').Length;

level = ((beginDirectory.Split ('\\') [1] == "") && (currentDirectory.Split ('\\') [1] != "")) ? level + 1 : level;

string formatPrefix = "|--";

for (int currentLevel = 0; currentLevel < level; currentLevel++) {
formatPrefix += "|--";
}
try {
foreach (string subdirectory in Directory.GetDirectories (currentDirectory)) {
Console.WriteLine (formatPrefix + subdirectory.Substring (subdirectory.LastIndexOf ('\\')));

DirectoryFileList (beginDirectory, subdirectory);
}

foreach (string file in Directory.GetFiles (currentDirectory)) {
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine (formatPrefix + Path.GetFileName (file));
Console.ForegroundColor = ConsoleColor.White;
}
} catch (UnauthorizedAccessException) {
System.Console.WriteLine ("发现受保护文件目录,禁止访问。");
} catch (Exception exception) {
System.Console.WriteLine ("程序运行异常:" + exception);
}
}
}
}

throw

Throw会抛出/传递程序执行期间出现异常的信号,通过在catch块里使用throw语句.可以改变产生的异常,比如我们可以抛出一个新的异常。

C#允许开发人员从代码中引发异常,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;

public class ThrowingExceptions {
public static void Main () {
try {
Console.WriteLine ("Begin executing");
Console.WriteLine ("Throw exception");
throw new Exception ("自定义异常");
Console.WriteLine ("End executing"); //永远不会执行
} catch (FormatException exception) {
Console.WriteLine (
"A FormateException was thrown");
} catch (Exception exception) {
Console.WriteLine (
"Unexpected error: {0}", exception.Message);
} catch //永远不会执行
{
Console.WriteLine ("Unexpected error!");
}

Console.WriteLine (
"Shutting down...");
}
}
/* 输出
Begin executing
Throw exception
Unexpected error: 自定义异常
Shutting down...
*/

有时catch块能捕捉到异常,但不能正确或者完整处理它。这种情况下,可以让这个catch块重新抛出异常,据提办法时使用一个单独的throw语句,不要在它后面指定任何异常。

throw;throw exception;的区别:throw; 保持了异常中的“调用栈”信息,而throw exception;将那些信息替换成当前调用栈信息。如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;

class Program {
static void Main () {
try {
X ();
} catch (Exception ex) {
Console.WriteLine ("X throw:" + ex.TargetSite);
}

try {
Y ();
} catch (Exception ex) {
Console.WriteLine ("Y throw:" + ex.TargetSite);
}
}

static void X () {
try {
int.Parse ("?");
} catch (Exception) {
throw; // Rethrow 构造
}
}

static void Y () {
try {
int.Parse ("?");
} catch (Exception ex) {
throw ex; // Throw 捕获的ex变量
}
}
}
/*
X throw:Void StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean)
Y throw:Void Y()
*/

throw; | throw exception; | throw new exception; 区别示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using System;

class hello {
public static void Main () {
ExceptionClass ec = new ExceptionClass ();

try {
ec.ExceptionThrow1 ();
} catch (Exception ex) {
Console.WriteLine (ex.ToString ());
}

try {
ec.ExceptionThrow2 ();
} catch (Exception ex) {
Console.WriteLine (ex.ToString ());
}

try {
ec.ExceptionThrow3 ();
} catch (Exception ex) {
Console.WriteLine (ex.ToString ());
}

try {
ec.ExceptionThrow4 ();
} catch (Exception ex) {
Console.WriteLine (ex.ToString ());
}

Console.ReadKey ();
}
}

public class ExceptionClass {
public void ExceptionThrow1 () {
try {
// 调用原始异常抛出方法来抛出异常
this.ExceptionMethod ();
} catch (Exception ex) {
throw ex;
}
}

public void ExceptionThrow2 () {
try {
this.ExceptionMethod ();
} catch (Exception ex) {
throw;
}
}

public void ExceptionThrow3 () {
try {
this.ExceptionMethod ();
} catch {
throw;
}
}

public void ExceptionThrow4 () {
try {
this.ExceptionMethod ();
} catch (Exception ex) {
throw new Exception ("经过进一步包装的异常", ex);
}
}

private void ExceptionMethod () {
throw new DivideByZeroException ();
}
}
/* 输出
System.DivideByZeroException: Attempted to divide by zero.
at ExceptionClass.ExceptionThrow1() in d:\WaProj\CSharpGuide\Program.cs:line 47
at hello.Main() in d:\WaProj\CSharpGuide\Program.cs:line 8
System.DivideByZeroException: Attempted to divide by zero.
at ExceptionClass.ExceptionMethod() in d:\WaProj\CSharpGuide\Program.cs:line 88
at ExceptionClass.ExceptionThrow2() in d:\WaProj\CSharpGuide\Program.cs:line 58
at hello.Main() in d:\WaProj\CSharpGuide\Program.cs:line 14
System.DivideByZeroException: Attempted to divide by zero.
at ExceptionClass.ExceptionMethod() in d:\WaProj\CSharpGuide\Program.cs:line 88
at ExceptionClass.ExceptionThrow3() in d:\WaProj\CSharpGuide\Program.cs:line 69
at hello.Main() in d:\WaProj\CSharpGuide\Program.cs:line 20
System.Exception: 经过进一步包装的异常 ---> System.DivideByZeroException: Attempted to divide by zero.
at ExceptionClass.ExceptionMethod() in d:\WaProj\CSharpGuide\Program.cs:line 88
at ExceptionClass.ExceptionThrow4() in d:\WaProj\CSharpGuide\Program.cs:line 78
--- End of inner exception stack trace ---
at ExceptionClass.ExceptionThrow4() in d:\WaProj\CSharpGuide\Program.cs:line 80
at hello.Main() in d:\WaProj\CSharpGuide\Program.cs:line 26
*/

结尾

0%