《C#本质论》 第5章 方法和参数

第5章 方法和参数

需要新的思维来管理这种复杂性程序,==“过程式”“结构化”==编程的基本思路就是提供对语句分组来构成单元的构造。此外,可通过结构化编程将数据传给一个语句分组,在这些语句执行完毕后返回结果。

5.1 方法的调用

初学者主题:什么是方法

设计规范

•方法总是和类型关联。类型将相关方法分为一组。

•方法通过“实参”接受数据,实参由方法的参数或形参定义。参数是调用者用于向被调用的方法传递数据的变量。方法通过返回值将数据返回调用者。

•方法调用由方法名称和实参列表构成。完全限定的方法名称包括命名空间类型名方法名、每部分以句点风格。

5.1.1 命名空间

命名空间是一种分类机制,用于分组功能相关的所有类型。命名空间是分级的、级数任意,一般不超过6级。

几乎所有的C#程序都要使用System 命名空间中的类型。

•调用方法并非一定要提供命名空间。例如调用的方法与发出调用的方法在同一个命名空间,就没必要指定命名空间。

​ 表5.1 常用命名空间

命名空间描述
System包含基元类型、以及用于类型转换、数学计算、程序调用以及环境管理的类型
System.Collection.Generics包含使用泛型的强类型集合
System.Drawing包含在显示设备上绘图和进行图象处理的类型
System.IO包含用于文件和目录处理类型
System.Linq包含使用“语言集成查询”(LINQ)对几何数据进行查血的类和接口
System.Text包含用于处理字符串和各种文本编码的类型,以及在不同编码方式之间转换的类型
System.Text.RegularExpressions包含用于处理正则表达式的类型
Systme.Threading 包含用于多线程编程的类型
System.Web包含用于实现浏览器到服务器通信的类型(一般通过HTTP进行)。该命名空间中的功能用于支持ASP.NET
System.Windows包含用WPF创建富用户界面的类型,WPF用XAML进行声明性UI设计
System.Xml为XML处理提供基于标准的支持。

设计规范

•要为命名空间使用PascalCase大小写 *考虑组织源代码文件目录结构以匹配命名空间层次结构。

5.1.2 类型名称

类型本质是对方法及其相关数据进行分组的一种方式。例如,Console类型包含常用的Write()、ReadLine()方法。所有这些方法都是在同一个组内。

5.1.3 作用域

一个事物的“作用域”是可用非限定名称引用它的那个区域,两个方法在同一个类型了中声明,一个方法调用另一个就不需要类型限定符,因为这两个方法具有整个包容类型的作用域。

5.1.4 方法名称

每个方法调用都要指定一个方法名称。它可能用可能不用命名空间和类型名称加以限定。

5.1.5 形参和实参

调用者为形参提供的值称为实参,每个实参都要和一个形参形成对应。

5.1.6 方法返回值

System.Console.ReadLine()没有任何参数,因为该方法声明为不获取任何参数。但这个方法有返回值。可利用返回值将调用方法所产生的结果返回调用者。

class Program
{
   static void Main()
   {
    System.Console.Write("Enter your first name:");
    System.Console.WriteLine("Hello {0}!",System.Console.ReadLine());
   }
}

5.1.7 对比语句和方法调用

虽然在一个语句中包含多个方法调用能减少编程量,但不一定能增强可读性,而且很少带来性能上的优势。开发者应该更注重代码的可读性,而不要将过多的精力放在写简短的代码上。

注意:为了使代码一目了然,进而在长时间里更加容易维护,可读性是关键。

5.2 方法的声明

5.2.1 参数声明

可在方法声明的圆括号中添加参数列表。列表中的每个参数都包含参数类型和参数名称,每个参数以逗号分隔。

设计规范

要为参数名使用camelCase大小写。

5.2.2 方法返回类型声明

使用return之后,在return以后的语句可能==“不可到达”==,编译器会发出警告。 C#允许提前返回,但是为了代码的可读性,且使代码更易维护,应尽量确定单一的退出位置,而不是在方法的多个代码路径中散步多个return语句。

5.2.3 表达式主体方法

有些方法过于简单,为简化这些方法定义,允许用表达式代替完整方法主体。

static string GetFullName(string firstName,string lastName)=>

表达式主体方法不是用大括号定义方法主体,而是用=>操作符。该操作符的结果数据类型必须与方法返回类型匹配。换言之,虽然没有显式的return语句,但表达式本身的返回类型必须与方法声明的返回类型匹配。

表达式主体方法是大量方法主体声明的语法快捷方法。正因如此,其应用应限于最简单的方法实现,例如单行表达式。

5.3 using指令

完全限定的名称可能很长、很笨拙。可将一个或多个命名空间的所有类型“导入”文件,这样在使用时就不需要完全限定。这可通过using指令(通常在文件顶部)来实现。

System.Console.WriteLine("Hello World");

等价于:

using System;
class HelloWorld
{
   static void Main()
   {
    Console.WriteLine("Hello World");
   }
}

虽然添加了using System,但使用System的某个子命名空间中的类型时还是不能省略System。例如,要访问System.Text中的StringBuilder类型,必须另外添加一个using System.Text指令或者对类型进行完全限定(System.Text.StringBuilder),而不能只是写Text.StringBuilder。简单地说,using指令不“导入”任何嵌套命名空间中的类型。嵌套命名空间(由命名空间中的句点符号来标识)必须显式导入。

高级主题:嵌套using指令

using指令不仅可以在文件顶部使用,还可以在命名空间声明的顶部使用。

namespace EssentialCSharp
{
   using System;
   class HelloWorld
   {
     static void Main()
     {
      Console.WriteLine("Hello World");
     }
   }
}

在文件顶部和命名空间声明的顶部使用using指令的区别在于,后者的using指令在声明的命名空间内有效。只如果在EssentialCSharp命名空间前后声明了新命名空间,新命名空间不会受别的命名空间中的using System指令的影响。但我们很少写这样的代码,特别是根据约定,每个文件只应该有一个类型声明。

5.3.1 using static 指令

using允许省略命名空间限定符来简化类型名称。而using static 指令允许将命名空间和类型名称都省略,只需写静态成员名称。例如WriteLine()。

using static System.Console;
class HeiYou
{
   static void Main()
   {
    string firstName;
    string lastName;
    
    WriteLine("Hey you");
   }
}

5.3.2 使用别名

还可利用using指令为命名空间或类型取一个别名。别名是在using指令起作用的范围内可以使用的替代名称。别名的两个最常见的用途是消除两个同名类型的歧义和缩写长名称。例如在代码清单5.10中,CountDownTimer别名引用了System.Timers.Timer类型。仅添加using System.Timers指令不足以完全限定Timer类型,原因是System.Threading也包含Timer类型,所以在代码中直接用Timer会产生歧义。

using System;
using System.Threading;
using CountDownTimer=System.Timers.Timer;

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

5.4 Main()的返回值和参数

到目前为止,可执行体的所有Main()方法采用的都是最简单的声明。这些Main()方法声明不包含任何参数或非void返回类型。

但C#支持在执行程序时提供命令行参数,并允许从Main()方法返回状态标识符。“运行时”通过一个string数组参数将命令行参数传给Main()。要获取参数,访问数组就可以了,

代码清单5.12对此进行了演示。程序的目的是下载指定URL位置的文件。第一个命令行参数指定URL,第二个指定存盘文件名。代码从一个switch语句开始,根据参数数量(args.Length)采取不同操作:1.如果没有两个参数,就显示一条错误消息,指出必须提供URL和文件名;2.如果有两个参数,表明用户提供了URL和存盘文件名。

using System;
using System.IO;
using System.Net.Http;

class Program
{
   static int Main(string[] args)
   {
    int resulut;
    switch(args.Length)
    {
      default:
     	 Console.WriteLine("错误:你必须提供网址和文件名");
    	 Console.WriteLine("有效用:下载网址和文件");
     	 result=1;
     	 break;
      case 2:
         WebClient webClient=new WebClient();
         webClient.DownloadFile(args[0],args[1]);
         result=0;
         break;
    }
    return result;
   }
}

成功获取存盘文件名,就用它保存下载的文件。否则应显示帮助文本。Main()方法还会返回一个int,而不是像往常那样返回void。返回值对于Main()声明来说是可选的。但如果有返回值,程序就可以将状态码返回给调用者(比如脚本或批处理文件)。根据约定,非零返回值代表出错。

5.5 高级方法参数

之前一直通过方法的return语句返回数据。本届描述方法如何通过自己的参数返回数据。

5.5.1 值参数

参数默认传值,参数值会拷贝到目标参数中。

class Program
{
    static void Main()
    {
      int num=10;
      int num2=Combine(num);
      Console.WriteLine(num2);
    }
    static int Combine(string num)
    {
      int nummber;
      number=num-5;
      return number;
    }
}

Combine()方法返回后,Main()中对应的变量仍会保持其初始值不变,因为在调用方法时,只是将变量的值拷贝了一份给方法。调用栈在一次调用的末尾“展开”的时候,拷贝的数据会被丢弃。

初学者主题:匹配调用者变量与参数名

调用者的变量名与被调用方法中的参数名相匹配,这是为了增强可读性。

高级主题:比较引用类型和值类型

5.5.2 引用参数(ref)

class Program
{
  static void Main()
  {
    string first="hello";
    string second="goodbye";
    Swap(ref first,ref second);
    Cosole.WriteLine(first);
    Cosole.WriteLine(second);
  }
  static void Swap(ref string x,ref string y)
  {
    string temp=x;
    x=y;
    y=temp;
  }
}

结果是:

goodbye
hello

如果被调用的方法将参数指定为ref,调用者调用该方法时提供的实参应该是附加了ref前缀的变量而不是值。这样调用者就显式确认了目标方法可对它接收到的任何ref参数进行重写赋值。此外,调用者应该初始化传引用的局部变量,因为被调用的方法可能从ref参数读取数据而不先对其赋值。 换言之、作用只是为现有变量分配参数名,而非创建新变量并将实参的值拷贝给它。

5.5.3 输出参数(out)

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter05.Listing05_15
{
    using System;

    public class ConvertToPhoneNumber
    {
        public static int Main(string[] args)
        {
            if(args.Length == 0)
            {
                Console.WriteLine(
                    "ConvertToPhoneNumber.exe <phrase>");
                Console.WriteLine(
                    "'_' indicates no standard phone button");
                return 1;
            }
            foreach(string word in args)
            {
                foreach(char character in word)
                {
#if !PRECSHARP7
                    if(TryGetPhoneButton(character, out char button))
#else
                    char button;//未被初始化
                    if(TryGetPhoneButton(character, out button))
#endif // PRECSHARP7
                    {
                        Console.Write(button);
                    }
                    else
                    {
                        Console.Write('_');
                    }
                }
            }
            Console.WriteLine();
            return 0;
        }

        static bool TryGetPhoneButton(char character, out char button)
        {
            bool success = true;
            switch(char.ToLower(character))
            {
                case '1':
                    button = '1';
                    break;
                case '2':
                    button = '2';
                    break;
                case '3':
                    button = '3';
                    break;
                case '4':
                    button = '4';
                    break;
                case '5':
                    button = '5';
                    break;
                case '6':
                    button = '6';
                    break;
                case '7':
                    button = '7';
                    break;
                case '8':
                    button = '8';
                    break;
                case '9':
                    button = '9';
                    break;
                case '0':
                    button = '0';
                    break;

                case 'a':
                case 'b':
                case 'c':
                    button = '2';
                    break;
                case 'd':
                case 'e':
                case 'f':
                    button = '3';
                    break;
                case 'g':
                case 'h':
                case 'i':
                    button = '4';
                    break;
                case 'j':
                case 'k':
                case 'l':
                    button = '5';
                    break;
                case 'm':
                case 'n':
                case 'o':
                    button = '6';
                    break;
                case 'p':
                case 'q':
                case 'r':
                case 's':
                    button = '7';
                    break;
                case 't':
                case 'u':
                case 'v':
                    button = '8';
                    break;
                case 'w':
                case 'x':
                case 'y':
                case 'z':
                    button = '9';
                    break;
                case '+':
                    button = '0';
                    break;
                case '-':
                    button = '-';
                    break;
                default:
                    // Set the button to indicate an invalid value
                    button = '_';
                    success = false;
                    break;
            }
            return success;
        }
    }
}

如前所述,用作ref参数的变量必须在传给方法前赋值,因为被调用方法可能直接从变量中读取值。 这时更安全的做法是以传引用的方式传入一个未初始化的局部变量。 为此,代码需要用关键字out修饰参数类型。 注意 所有正常的代码路径都必须对out参数赋值

5.5.4 只读传引用(in)

int Method(in int number)
{
//
}

C# 7.2支持以传引用的方式传入只读值类型。该特性以传引用的方式传入值类型参数并且让该参数不能被方法修改。这样不仅避免了每次调用方法都创建值类型的拷贝,而且不用担心值类型参数被修改。换言之,其作用是在传值时减少拷贝量,同时把它标识为只读,从而增强性能。该语法要为参数添加in修饰符。

5.5.5 返回引用

byte[] _Image;
public ref byte[] Image { get { return ref _Image};}

返回引用有两个重要的限制,两者都和对象生存期有关:(1)对象仍被引用时不应被垃圾回收;(2)对象的所有引用都消失之后,不应再占用内存。

为符合这些限制,从方法返回引用时只能返回:

•对字段或数组元素的引用。

•其他返回引用的属性或方法。

•作为参数传给“返回引用的方法”的引用。

5.5.6 参数数组

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter05.Listing05_17
{
    using System;
    using System.IO;

    public class Program
    {
        public static void Main()
        {
            string fullName;

            // ...

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

            // ...

            // Call Combine() with only three parameters
            fullName = Combine(
                Directory.GetParent(Directory.GetCurrentDirectory()).FullName,
                "Temp", "index.html");
            Console.WriteLine(fullName);

            // ...

            // Call Combine() with an array
            fullName = Combine(
                new string[] {
                    $"C:{Path.DirectorySeparatorChar}", "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;
        }
    }
}

参数数组不一定是方法的唯一参数,但必须是最后一个,由于只能放在最后,所以最多只能有一个参数数组。

  • 调用者可指定和参数数组对应的零个实参,这会使传递的参数数组包含零个数据项

  • 参数数组是类型安全——实参类型必须兼容参数数组的类型

  • 调用者可传递一个实际的数组,而不是传递以逗号分隔的参数列表,最终生成的CIL代码一样。

  • 如目标方法的实现要求一个最起码的参数数量,请在方法声明中显式指定必须提供的参数。这样一来,遗漏必须参数会导致编译器报错。

    设计规范

  • 能处理任何数量包括零个额外实参的方法要使用参数数组。

5.6递归

M(x)
{
  if x已达最小,不可继续分解[1]
    返回结果
  else
    (1) 采取一些操作使问题变得更小
    (2) 递归调用M来解决更小的问题
    (3) 根据(1)和(2)计算结果
    返回结果
}

初学者主题:无限递归错误 不遵守模式就可能出错,例如不能将问题变得更小,或者不能处理所有可能的最小情况,就会无限递归。

5.7 方法重载

一个类的所有方法都必须有唯一签名,C#根据方法名参数数据类型参数数量的差异来定义唯一性。注意方法返回类型不计入签名。两个方法只是返回类型不同会造成编译错误。 如一个类包含两个或者多个同名方法,就会发生方法重载。对于重载的方法,参数数量或数据类型肯定不同

**注意 ** 方法的唯一性取决于方法名、参数数据类型或参数数量的差异

方法重载是一种操作性多态。如由于数据变化造成同一个逻辑操作具有许多形式,就会发生多态。 基本思路是开发者只需在一个方法中实现核心逻辑,其他所有重载版本都调用那个方法。

注意 将核心功能放在单一方法中供其他重载方法调用,以后就只需要在核心方法中修改,其他方法将自动收益。

5.8 可选参数

可选参数

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter05.Listing05_20
{
    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]);
            }
            else if(args.Length > 0)
            {
                totalLineCount = DirectoryCountLines(args[0]);
            }
            else
            {
                totalLineCount = DirectoryCountLines();
            }

            System.Console.WriteLine(totalLineCount);
        }

        static int DirectoryCountLines()
        {
            return 0;

            // ...
        }

        /*
          static int DirectoryCountLines(string directory)
          { ... }
        */

        static int DirectoryCountLines(
            string directory, string extension = "*.cs")
        {
            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 is object)
            {
                if (line.Trim() != "")
                {
                    lineCount++;
                }
                line = reader.ReadLine();
            }

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

C#中,string? 表示可空的字符串类型。这意味着该变量可以存储字符串值,也可以存储空值(null)。这是C# 8.0 中引入的一种新的语法,用于表示可空引用类型。

具名参数

调用者可利用具名参数为一个参数显式赋值,而不是像以前那样只能依据参数顺序来决定哪个值赋给哪个参数。

using System.IO;
class Program
{
   static void Main()
   {
     DisplayGreeting(first:"Inigo",lastName:"Montoya");
   }
   public static void DisplayGreeting(
   string firstName,
   string? middleName = null,
   string? lastName = null)
   {
   //……
   }
}

设计规范

要将参数名视为API的一部分;要强调API之间的版本兼容性,就避免改变名称。

高级主题:方法解析

编译器从一些列“适用”的方法中选择最终调用的方法时,依据的是那个方法最具体。假定有两个适用的方法,每个都要求将实参隐式转化为形参的类型,最终选择的是形参类型最具体(派生程度最大)的方法。 例如double比object更具体,有不是double的object,但没有不是object的double,所以double更具体。

5.9 用异常实现基本错误处理

初学者主题:42作为字符串和整数 二者类型不同,完全不一样,字符串由4和2两个字符构成,而int 42是数值42.

5.9.1 捕捉错误

#pragma warning disable CS1058 // A previous catch clause already catches all exceptions
#pragma warning disable CS0168 // Variable is declared but never used
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter05.Listing05_25
{
    public class ExceptionHandling
    {
        public static int Main()
        {
            string firstName;
            string ageText;
            int age;
            int result = 0;

            System.Console.Write("Enter your first name: ");
            firstName = System.Console.ReadLine();

            System.Console.Write("Enter your age: ");
            ageText = System.Console.ReadLine();

            try
            {
                age = int.Parse(ageText);
                System.Console.WriteLine(
                    $"Hi { firstName }!  You are { age * 12 } months old.");
            }
            catch(System.FormatException exception)
            {
                System.Console.WriteLine(
                    $"The age entered ,{ageText}, is not valid.");
                result = 1;
            }
            catch(System.Exception exception)
            {
                System.Console.WriteLine(
                    $"Unexpected error:  { exception.Message }");
                result = 1;
            }
            catch
            {
                System.Console.WriteLine("Unexpected error!");
                result = 1;
            }
            finally
            {
                System.Console.WriteLine($"Goodbye { firstName }");
            }

            return result;
        }
    }
}

未提供任何异常处理,所以程序会向用户报告发生了**“未处理的异常”。系统中没有注册任何调试器,错误信息会出现在控制台上。 显然,这样的错误消息并不是特别有用,需要提供一个机制对错误进行恰当的处理,例如向用户报告一条更有意义的错误信息。 这个过程称为捕捉异常**。

首先用try块将可能抛出异常的代码包围起来,这个块以try关键字开始,try关键字告诉编译器,如果这里抛出了异常,那么某个catch块要尝试处理这个异常。catch块可指定异常的数据类型,只要数据类型与异常类型匹配,对应的catch块就会执行,但加入找不到合适的catch块,抛出的异常就会变成一个未处理的异常,就好像没有进行异常处理一样。

catch块的数量随意,但处理异常的顺序不要随意,catch块必须从具体到最不具体排列。System.Exception(异常)数据类型最不具体,所以它应该放到最后System.FormatException排在第一,因为它是所处理的最具体的异常。

无论try块的代码是否抛出异常,只要控制离开try块,finally块都会执行.finally提供一个位置,无论是否异常都要执行的代码.finally块最适合用来执行资源清理. 可以只写一个try块和一个finally块,而不懈任何catch块。

设计规范

避免从finally块抛出异常,因方法调用而隐式抛出的异常可以接受。 *要优先使用try/finally 而不是try/catch块来实现资源清理代码。 *要在抛出的异常中描述异常为什么发生。顺带说明为何防范更佳。

高级主题:Exception类继承

异常类型描述
System.EXception最“基本”的异常,其他所有异常类型都从它派生。
System.ArgumentException传给方法的参数无效
System.ApplicationException避免使用该异常。最开始是想区分系统异常和应用程序异常。貌似合理,实际不好用。
System.FormatException实参类型不符合规范
System.IndexOutOfRangeException试图访问不存在的数组或其他集合元素。
System.InvalidCastException无效的类型转换。
System.InvalidOperationException发生非预期的情况,应用程序不再处于有效工作状态。
System.NotImplementedException虽然找到了对应的方法签名,但该方法尚未完全实现。
System.NullReferenceExcption引用为空,没有指向一个实例
System.ArithmeticException发生被零除以外的无效数学运算。
System.ArrayTypeMismatchException试图将类型有误的元素存储到数组中
System.StackOverflowException发生非预期的深递归。

高级主题:常规catch

可指定一个无参的catch块,称为常规catch块,等价于获取object数据类型的catch块,例如catch(objectexception),由于所有类最终都从object派生,所有无参的catch块必须放在最后。

设计规范

•避免使用常规catch块,用捕捉System.Exception的catch块代替。

•避免捕捉无法从中完全恢复异常,这种异常不处理比不正确处理好。

•避免在重新抛出前捕捉和记录异常。要允许异常逃脱,直至它被正确处理。

5.9.2 使用throw语句报告错误

C#允许开发人员从代码中抛出异常,抛出异常需要有Exception的实例。

设计规范

•要在捕捉并重新抛出异常时使用空的throw语句,以便保留调用栈。

•要通过抛出异常而不是返回错误码来报告执行失败。

•不要让公共成员将异常作为返回值或者out参数,抛出异常来指明错误,不要把它们作为返回值来指明错误。

避免使用异常处理来处理预料之中的情况

异常是为了跟踪例外的、事先没有料到的、可能造成严重后果的情况设计的。

高级主题:

使用TryParse()执行数值交换。

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值