# 三、方法和属性

前面几章展示了如何在`Main`方法中编写代码。这是程序入口点，但它通常是一个轻量级方法，没有太多代码。在本章中，您将学习如何将代码移出`Main`方法并模块化，以便更好地管理代码。您将学习如何定义带有参数和返回值的方法。您还将了解属性，它允许您封装对象状态。

## 从主车道开始

我们将使用上一章中计算器的简单版本来开始。这个计算器只执行加法，一次运算后停止运行。

```cs
    using System;

    class Calculator1
    {
        static void Main()
        {
            Console.Write("First Number: ");
            string firstNumberInput = Console.ReadLine();
            double firstNumber = double.Parse(firstNumberInput);

            Console.Write("Second Number: ");
            string secondNumberInput = Console.ReadLine();
            double secondNumber = double.Parse(secondNumberInput);

            double result = firstNumber + secondNumber;

            Console.WriteLine($"\n\tYour result is {result}.");

            Console.ReadKey();
        }
    }

```

代码清单 39

这个程序中可能是新的部分是`Main`方法末尾的`Console.ReadKey`语句。这允许用户看到结果，并防止程序结束，直到他们按下一个键。内插字符串中的`\n`是一个换行符，`\t`是一个制表符。

## 用方法模块化

虽然之前的程序很小，但第一眼并不能真正告诉你它是做什么的。想象一下，如果它像第二章中的[计算器，甚至更长；最终会变得难以理解。当您稍后必须再次处理这个问题时，您可能需要阅读许多行代码才能理解它。所以，重构这个会更好。重构是改变代码设计而不改变其功能的实践；目的是改进程序。下面的代码示例是将该程序重构为方法的初稿。](2.html#CodeListing34)

```cs
    using System;

    class Calculator2
    {
        static void Main()
        {
            double firstNumber = GetFirstNumber();

            double secondNumber = GetSecondNumber();

            double result = AddNumbers(firstNumber, secondNumber);

            PrintResult(result);

            Console.ReadKey();
        }

        static double GetFirstNumber()
        {
            Console.Write("First Number: ");
            string firstNumberInput = Console.ReadLine();
            double firstNumber = double.Parse(firstNumberInput);
            return firstNumber;
        }

        static double GetSecondNumber()
        {
            Console.Write("Second Number: ");
            string secondNumberInput = Console.ReadLine();
            double secondNumber = double.Parse(secondNumberInput);
            return secondNumber;
        }

        static double AddNumbers(double firstNumber, double secondNumber)
        {
            return firstNumber + secondNumber;
        }

        static void PrintResult(double result)
        {
            Console.WriteLine($"\nYour result is {result}.");
        }
    }

```

代码清单 40

看`Main`，可以知道程序是做什么的。它读取两个数字，将结果相加，然后向用户显示结果。每一行都是一个方法调用。前三种方法——`GetFirstNumber`、`GetSecondNumber`和`AddNumbers`——返回一个分配给变量的值。最后一个方法`PrintResult`执行一个操作，但不返回结果。在进行下一次重构之前，让我们先来看看这些方法。下面的代码清单显示了`GetFirstNumber`方法。

```cs
        static double GetFirstNumber()
        {
            Console.Write("First Number: ");
            string firstNumberInput = Console.ReadLine();
            double firstNumber = double.Parse(firstNumberInput);
            return firstNumber;
        }

```

代码清单 41

乍一看，这个方法的签名看起来类似于`Main`方法。不同的是这个方法的返回类型是`double`，方法名为`GetFirstNumber`。我们所做的只是编写创建`firstNumber`的方法和代码。当方法具有返回类型时，必须返回该类型的值。`GetFirstNumber`用`return`语句做到这一点。

`GetSecondNumber`方法与`GetFirstNumber`几乎相同。接下来让我们来看看`AddNumbers`。

```cs
        static double AddNumbers(double firstNumber, double secondNumber)
        {
            return firstNumber + secondNumber;
        }

```

代码清单 42

请注意，`Main`将`firstNumber`和`secondNumber`变量作为参数传递给`AddNumbers`，`AddNumbers`方法可以使用这些参数。`AddNumbers`的返回类型为`double`，所以该方法进行加法运算，返回加法运算的结果。

最后，我们有`PrintResult`法。

```cs
        static void PrintResult(double result)
        {
            Console.WriteLine($"\nYour result is {result}.");
        }

```

代码清单 43

`PrintResult`方法将参数结果写入控制台。注意`PrintResult`没有返回类型，如`void`关键字所示。

## 用方法简化代码

最后一部分改进了程序，因为一大块代码被分成了更有意义的部分。我们可以通过一些额外的重构来改进这段代码。特别是`GetFirstNumber`和`GetSecondNumber`方法在很大程度上是多余的。下面的示例展示了如何将这两种方法重构为一种，并减少代码量。

```cs
    using System;

    class Calculator3
    {
        static void Main()
        {
            double firstNumber = GetNumber("First");
            double secondNumber = GetNumber("Second");

            double result = AddNumbers(firstNumber, secondNumber);

            PrintResult(result);

            Console.ReadKey();
        }

        static double GetNumber(string whichNumber)
        {
            Console.Write($"{whichNumber} Number: ");
            string numberInput = Console.ReadLine();
            double number = double.Parse(numberInput);
            return number;
        }

        static double AddNumbers(double firstNumber, double secondNumber)
        {
            return firstNumber + secondNumber;
        }

        static void PrintResult(double result)
        {
            Console.WriteLine($"\nYour result is {result}.");
        }
    }

```

代码清单 44

这次我去掉了`GetFirstNumber`和`GetSecondNumber`，换成了`GetNumber`。除了变量名之外，唯一真正的区别是`whichNumber` `string`参数。

## 添加属性

前面的例子执行了同一个类内部的所有操作。它由`Main`方法驱动，并通过方法进行服务。如果我想重用该类中的计算器函数，并希望新类保存自己的值或状态，该怎么办？在这种情况下，将计算器方法移到单独的`Calculator`类中会很有用。

接下来要问的问题是，“我们如何进入班级的状态？”比如我想读`Calculator`课的成绩，最好的方法是什么？一种方法是使用名为`GetResult`的方法返回该值。C# 中的另一种方法是使用属性，它可以像字段一样使用，但像方法一样工作。下面版本的计算器程序展示了如何将方法重构到一个单独的类中并添加属性。

| ![](img/note.png) | 注意:重构是改变代码设计而不改变其行为的实践，目的是改进代码。马丁·福勒的书《重构:改进现有代码的设计》是一个很好的参考。 |

```cs
    using System;

    public class Calculator4
    {

        double[] numbers = new double[2];

        public double First
        {
            get
            {
                return numbers[0];
            }
        }

        public double Second
        {
            get
            {
                return numbers[1];
            }
        }

        double result;

        public double Result
        {
            get { return result; }
            set { result = value; }
        }

        public void GetNumber(string whichNumber)
        {
            Console.Write($"{whichNumber} Number: ");
            string numberInput = Console.ReadLine();
            double number = double.Parse(numberInput);

            if (whichNumber == "First")
                numbers[0] = number;
            else
                numbers[1] = number;
        }

        public void AddNumbers()
        {
            Result = First + Second;
        }

        public void PrintResult()
        {
            Console.WriteLine($"\nYour result is {result}.");
        }
    }

```

代码清单 45

在前面的代码清单中，`First`、`Second`和`Result`是属性。我将很快分解语法，但是首先看看这些属性在`AddNumbers`和`PrintResults`方法中是如何使用的。`AddNumbers`读取`First`和`Second`的值，并将这些值相加并写入`Result`。

每个属性看起来都像一个字段或变量；你只是读和写给他们。`PrintResult`读取`Result`属性。但是，查看属性的定义，您可以立即知道它们不是字段。

`Result`属性是一个典型的带有`get`和`set`访问器的读写属性。读取属性时，`get`访问器执行。当您写入属性时，`set`存取器执行。注意在`Result`(大写)属性之前有一个`result`字段(小写)。`get`存取器读取`result`的值，`set`存取器写入`result`。使用`set`时，`value`关键字代表正在写入属性的内容。

这种从单个后备存储中读写的模式非常常见，以至于 C# 有一种快捷语法可以替代。下面的代码示例显示了被重写为自动实现的属性的`Result`。

```cs
        public double Result { get; set; }

```

代码清单 46

自动实现的属性中的后备存储由 C# 编译器在幕后隐含和处理。如果您需要对所分配的值进行验证，或者有一种独特的方法来存储该值，那么您应该使用定义了`get`访问器、`set`访问器或两者的完整属性。

事实上，`First`和`Second`属性有一个唯一的后备存储，需要一个完全实现的`get`访问器。它们从数组位置读取。注意`GetNumber`方法计算出每个数字放入哪个数组位置。

属性使您能够封装类的内部操作，因此您可以自由修改实现，而不会破坏类使用者的接口。下面的代码示例演示了消费代码如何使用这个新的`Calculator4`类。

```cs
    using System;

    class Program
    {
        static void Main()
        {
            var calc4 = new Calculator4();

            calc4.GetNumber("First");
            calc4.GetNumber("Second");

            calc4.AddNumbers();

            PrintResult(calc4);

            Console.ReadKey();
        }

        static void PrintResult(Calculator4 calc)

        {
        Console.WriteLine();
        Console.WriteLine($"Your result is {result}.");
    }
    }

```

代码清单 47

`Main`方法创建`Calculator4`的新实例，并调用公共方法。`Calculator4`所有奇怪的内部都是隐藏的，`Main`只关心公共接口，暴露`Calculator4`服务供重用。`PrintResult`方法读取`Calculator4`实例`Result`属性。同样，这也是方法和属性的好处:调用者可以使用一个类，而不用关心这个类是如何工作的。

## 异常处理

C# 有一个称为结构化异常处理的特性，它允许您处理方法无法实现预期目的的情况。管理异常处理的语法是`try` - `catch`块。所有监控异常的代码都在`try`块中，处理潜在异常的代码在`catch`块中。下面的代码清单显示了一个示例。

```cs
            static void HandleNullReference()
            {
                Program prog = null;

                try
                {
                    Console.WriteLine(prog.ToString());
                }
                catch (NullReferenceException ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }

```

代码清单 48

在 C# 中，任何时候你试图使用`null`对象的成员，你都会收到一个`NullReferenceException`。解决这个问题的方法是给变量赋值。前面的例子通过调用`prog`变量`null`上的`ToString`导致一个`NullReferenceException`被扔进`try`块。

由于抛出异常的代码在`try`块中，因此该代码停止执行`try`块中的任何代码，并开始寻找异常处理程序。`catch`块`parameter`表示如果`try`块内部的代码抛出该异常类型，它可以捕捉到一个`NullReferenceException`。`catch`块的主体是您执行任何异常处理的地方。

您可以使用多个`catch`块自定义异常处理。以下示例显示了在`try`块中引发异常的代码，该异常随后由`catch`块处理。

```cs
            static void HandleUncaughtException()
            {
                Program prog = null;

                try
                {
                    Console.WriteLine(prog.ToString());
                }
                catch (ArgumentNullException ex)
                {
                    Console.WriteLine("From ArgumentNullException: " + ex.Message);
                }
                catch (ArgumentException ex)
                {
                    Console.WriteLine("From ArgumentException: " + ex.Message);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("From Exception: " + ex.Message);
                }
                finally
                {
                    Console.WriteLine("Finally always executes.");
                }
            }

```

代码清单 49

方法名为`HandleUncaughtException`，因为没有具体的`catch`块来处理一个`NullReferenceException`；例外情况将由`Exception`类型的`catch`模块处理。

您可以按继承层次列出异常，顶层异常位于`catch`块列表的下方。抛出的异常将在处理程序列表中下移，寻找匹配的异常类型，并且只在匹配的第一个处理程序的`catch`块中执行。`ArgumentNullException`源于`ArgumentException`，`ArgumentException`源于`Exception`。

如果没有`catch`块可以处理异常，代码会在栈上寻找调用代码中可以处理异常类型的潜在`catch`块。如果调用栈中没有代码能够处理异常，您的程序将终止。

如果程序开始执行`try`块中的代码，则`finally`块始终执行。如果异常发生并且没有被捕获，`finally`块将在程序查看匹配的捕获处理程序的调用代码之前执行。

您可以编写一个`try` - `finally`块(没有`catch`块)，以保证一旦`try`块开始，某些代码将会执行。这对于打开资源(如文件或数据库连接)非常有用，并且可以保证无论是否发生异常，您都能够关闭该资源。

如果您遇到方法无法实现其预期目的的原因，则`throw`会出现异常。中有许多`Exception`派生类型。NET FCL，你可以使用。下面的代码示例汇集了一些您可能想要使用的概念，例如验证方法输入和抛出一个`ArgumentNullException`。

```cs
    public class Address
    {
        public string City { get; set; }
    }

    internal class Company
    {
        public Address Address { get; set; }
    }

            // Inside of a class...
            static void ThrowException()
            {
                try
                {
                    ValidateInput("something", new Company());
                }
                catch (ArgumentNullException ex) when (ex.ParamName == "inputString")
                {
                    Console.WriteLine("From ArgumentNullException: " + ex.Message);
                }
                catch (ArgumentException ex)
                {
                    Console.WriteLine("From ArgumentException: " + ex.Message);
                }
            }

            static void ValidateInput(string inputString, Company cmp)
            {
                if (inputString == null)
                    throw new ArgumentNullException(nameof(inputString));

                if (cmp?.Address?.City == null)
                    throw new ArgumentNullException(nameof(cmp));
            }

```

代码清单 50

前面的代码显示了一个`Address`类和一个`Company`类，它们的属性为`Address`类型。`ThrowException`消息的`try`块传递了`Company`的新实例，但没有实例化`Address`，这意味着`Company`实例的`Address`属性是`null`。

在`ValidateInput`中，`if`语句使用空引用运算符`?.`，检查`Company`、`Company`的`Address`属性或`Address`的`City`属性之间的值是否为`null`。这是一种方便的检查`null`的方法，不需要一组单独的检查，产生更少的语法和更简单的代码。如果这些值中有任何一个是`null`，代码将抛出`ArgumentNullException`。

`ArgumentNullException`的参数使用`nameof`运算符，该运算符计算传递给它的值的`string`表示；正是`"`T4`"`在这种情况下。这段代码没有包含在`try`块中，所以控制返回到调用这个方法的代码。

回到`ThrowException`方法，抛出的异常导致代码寻找适合该异常类型的处理程序。异常类型是`ArgumentNullException`，但是`ArgumentNullException`的`catch`块不会执行。这是因为`ArgumentNullException` `catch`块参数后面的`when`子句正在检查`"` `inputString` `"`中的一个`ParamName`。这个`when`子句叫做异常过滤器。如前所述，实例化时传递给`ArgumentNullException`的参数名是`"` `cmp` `"`，所以不匹配。因此，代码继续查看`catch`处理程序。

由于`ArgumentNullException`来源于`ArgumentException`，没有异常过滤器，`ArgumentException`的`catch`处理程序执行。现在异常被处理了。

| ![](img/tip.png) | 提示:抛出和处理特定的异常通常比它们的父异常更好。这为异常增加了更多的保真度和意义，并使调试更加容易。 |

## 总结

方法帮助您将代码组织成命名函数，您可以调用这些函数来执行操作。他们的名字记录了代码的功能。此外，方法有助于避免在多个地方重复相同的代码。从使用属性的类的代码的角度来看，属性像字段一样使用，看起来像字段。然而，属性更复杂，因为它们有`get`和`set`访问器，使它们像方法一样工作，并执行更复杂的工作，如验证或特殊值处理。方法和属性都有助于定义类对使用者的接口，并让您封装类的内部操作，这使得它更具可重用性。可以使用`try` - `catch`块处理异常，使用`try` - `finally`块保证关键代码执行。每当你正在编写的方法不能实现其预期目的时，使用`throw`语句。