Skip to content

Latest commit

 

History

History
430 lines (304 loc) · 18.2 KB

3.md

File metadata and controls

430 lines (304 loc) · 18.2 KB

三、方法和属性

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

从主车道开始

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

    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是一个制表符。

用方法模块化

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

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

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

代码清单 41

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

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

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

代码清单 42

请注意,MainfirstNumbersecondNumber变量作为参数传递给AddNumbersAddNumbers方法可以使用这些参数。AddNumbers的返回类型为double,所以该方法进行加法运算,返回加法运算的结果。

最后,我们有PrintResult法。

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

代码清单 43

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

用方法简化代码

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

    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

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

添加属性

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

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

| | 注意:重构是改变代码设计而不改变其行为的实践,目的是改进代码。马丁·福勒的书《重构:改进现有代码的设计》是一个很好的参考。 |

    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

在前面的代码清单中,FirstSecondResult是属性。我将很快分解语法,但是首先看看这些属性在AddNumbersPrintResults方法中是如何使用的。AddNumbers读取FirstSecond的值,并将这些值相加并写入Result

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

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

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

        public double Result { get; set; }

代码清单 46

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

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

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

    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块中。下面的代码清单显示了一个示例。

            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块中的任何代码,并开始寻找异常处理程序。catchparameter表示如果try块内部的代码抛出该异常类型,它可以捕捉到一个NullReferenceExceptioncatch块的主体是您执行任何异常处理的地方。

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

            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源于ArgumentExceptionArgumentException源于Exception

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

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

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

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

    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语句使用空引用运算符?.,检查CompanyCompanyAddress属性或AddressCity属性之间的值是否为null。这是一种方便的检查null的方法,不需要一组单独的检查,产生更少的语法和更简单的代码。如果这些值中有任何一个是null,代码将抛出ArgumentNullException

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

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

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

| | 提示:抛出和处理特定的异常通常比它们的父异常更好。这为异常增加了更多的保真度和意义,并使调试更加容易。 |

总结

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