了解C语言的方法和属性

129 阅读6分钟

在C#和其他编程语言中,方法被用来定义行为。它们被用来编写一些重点逻辑,用于完成一项特定的任务,并可以被调用,或被调用,以执行写在它们里面的代码。类中的方法有访问修饰符,默认为私有。这意味着该方法只可在同一个类内使用。要使该方法对整个项目可用,你可以使用内部关键字。如果你要使用public关键字,这就使得该方法在任何地方都可用。现在让我们多了解一下C#中的方法、字段、事件和属性。


返回类型

所有的方法都有一个返回类型。这是什么意思呢?当你调用一个方法时,它可以返回一个值或不返回。如果它不返回,它仍然有一个返回类型,我们称之为无效返回类型。如果一个方法的类型是无效的,它仅仅意味着这个方法不向调用的代码返回一个值。下面是一个返回类型为void的方法的例子。

static void WriteResult(string description, int result)
{
    Console.WriteLine(description + ": " + result);
}

当上面的代码被调用时,它没有返回一个值,因此它的类型是void。请注意,这里没有返回语句。然而,下面列出的代码确实使用了一个返回语句。当ComputeStatistics()方法运行时,它返回一个对象。

public StockStatistics ComputeStatistics()
{
    StockStatistics stats = new StockStatistics();

    float sum = 0;
    foreach (float stock in stocks)
    {
        stats.HighestStock = Math.Max(stock, stats.HighestStock);
        stats.LowestStock = Math.Min(stock, stats.LowestStock);
        sum += stock;
    }

    stats.AverageStock = sum / stocks.Count;
    return stats;
}

方法可以接受参数

方法在其方法签名或定义中可以有一个或多个参数。然而,不要把参数和论据混为一谈。在方法的定义代码中,我们将传递给方法的数据称为参数,而在方法实际被调用时,则称为参数。C#中的参数是类型化的。这意味着我们指定将被传递给方法的参数的数据类型。考虑一下这里的代码。
static void WriteResult(string description, float result)

{
    Console.WriteLine($"{description}: {result:F2}", description, result);
}

上面的代码需要两个参数。第一个的类型是字符串,并使用了description的名字。所以在这个方法里面,description要保持一个字符串的数据类型。它还有一个float类型的参数,名称为result。因此,同样,在函数内部,只要你看到result,你就知道它将包含一个float数据类型。


C#方法的签名

一个方法的签名由一个方法名和它的每个参数的类型和种类组成。这对C#编译器来说是一个唯一的标识符。方法的签名不包括返回类型。任何合法的字符都可以用在方法的名称中。方法名称通常以大写字母开头。所选择的名称应该是动词或动词后的形容词或名词。这有助于识别一个方法执行了一个动作。


方法可以被重载

方法是可以被重载的。这是什么意思呢?它意味着一个方法可以与另一个方法的名称完全相同,而且C#编译器也可以接受。然而,规则是,每个具有相同名称的方法必须在方法接受的参数类型或数量上有所不同。作为C#语言的一部分,重载方法的一个例子是常用的WriteLine()方法。在Visual Studio中,我们甚至可以看到intellisense给了我们一条信息:Console.WriteLine()有18个重载。
C# Example Overload Method

是签名使这些方法变得独一无二,尽管它们有相同的名字。所以你实际上是在根据你传递的是浮动值、int值还是字符串来调用不同的方法。

public static void WriteLine(float value);

public static void WriteLine(int value);

public static void WriteLine(string value);

让我们给我们的应用程序添加一个新的封装方法,名为CustomWriteLine()。这个方法将为我们做的是接受对程序结果的描述,以及结果本身的值。CustomWriteLine()方法的内部是对Console.WriteLine的调用,它使用字符串插值来输出数据。这使得我们可以自定义输出数据的格式。

上面的代码使用了一个字符串类型的私有字段。按照惯例,你经常会看到一个私有字段使用前面的下划线,以便直观地表明这是一个私有字段。所以这个字段在一个名为Dog的类中,在Dog的构造函数中,开发者需要传递一个名字以创建一个Dog对象。然后构造函数将名字保存在私有字段中,使其可供对象的其他部分使用。这个字段也是一个只读字段。这意味着只有这个类中的代码可以通过构造函数分配一个名字。试图在构造函数之外的任何其他方法中把名字设置为一个新的值,会被C#编译器抛出一个异常。

属性:在C#中,一个属性几乎就像一个字段,但它有获取器和设置器,这是一种非常酷的方式来管理在读取或写入该属性数据时发生的事情。这些访问器经常被用于验证,以确保数据的安全和干净。让我们在一些代码中看到这一点。

public class Dog
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            if (!String.IsNullOrEmpty(value))
            {
                _name = value;
            }
        }
    }
}

这段代码仍然有一个叫做_name的私有字段。不同的是,我们现在有一个叫做Name的属性。在C#中,属性和方法名称都应该以大写字母开头。注意这个属性中的get和set访问器。在get访问器中是一个简单的返回语句,返回_name中的值。返回的值需要是一个星号,因为该属性被定义为一个字符串。除了get访问器之外,还有一个set访问器。可以说,set访问器比get访问器更重要。这是因为这是验证逻辑可以被放置的地方,以确保正确的数据被存储在_name。我们不希望有人试图将_name设置为null或空字符串。这种类型的代码经常被用来在你把一个值分配给对象之前验证一个传入的值。

如果你愿意,你也可以利用C#中的自动实现的属性。这种类型的属性没有与之相关的逻辑。它只使用get和set关键字,没有额外的代码。C#编译器会自动创建一个字段来存储该属性的值。它将在获取操作中自动读取该字段,在设置操作中自动写到该字段。如果你不需要任何特殊的逻辑,但仍然希望得到getter和setter的好处,这就很方便。

public class Dog
{
    private string _name;
    public string Name
    {
        get;
        set;
    }
}

我们可以像这样把这种方法添加到我们的StockPortfolio类中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks
{
    public class StockPortfolio
    {
        public StockPortfolio()
        {
            stocks = new List<float>();
        }

        public StockStatistics ComputeStatistics()
        {
            StockStatistics stats = new StockStatistics();


            float sum = 0;
            foreach (float stock in stocks)
            {
                stats.HighestStock = Math.Max(stock, stats.HighestStock);
                stats.LowestStock = Math.Min(stock, stats.LowestStock);
                sum += stock;
            }

            stats.AverageStock = sum / stocks.Count;
            return stats;
        }

        public void AddStock(float stock)
        {
            stocks.Add(stock);
        }

        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                if (!String.IsNullOrEmpty(value))
                {
                    _name = value;
                }
            }
        }

        private string _name;

        private List<float> stocks;
    }
}

所以,在字段上设置属性的目的是为了让你可以在获取访问器或设置访问器中编写逻辑。get访问器可以对数据进行一些操作,或者只是检索一些字段的值并将其返回。set访问器可以进行验证,也可以保护对象的内部状态,以确保不会有人给你一个你不想要的值。属性可以维护我们程序中各种对象的状态。


C#事件

one publisher multiple subscribers
事件是C#编程语言的一个非常酷的功能。事件可以用来与应用程序的各个部分进行交互,这些部分在我们可能事先不知道的时候做事情。如果你熟悉JavaScript中的事件,这个概念也是类似的。想象一下,在一个应用程序的用户界面中的一个按钮。我们希望在用户点击该按钮的任何时候都能被告知,这样我们就可以在软件中采取某种行动。我们不知道用户什么时候会点击这个按钮,但我们确实需要在它发生时得到通知。另一种情况可能是监听某个时间的网络钩子。如果有一个POST请求被发送到某个端点,我们想得到通知,这样我们就可以采取一个行动。这些类型的场景对事件来说是很好的。事件允许对象向正在收听的应用程序的任何部分发出公告。公告就是事件,发布公告的对象就是该事件的发布者。在另一端是事件的订阅者。这是应用程序的一部分,它对收听任何事件公告感兴趣,然后采取一个行动。一个事件可以有很多订阅者。想象一下,一个用户点击一个按钮,结果是一个动作被记录到数据库中,一封电子邮件被发送给管理员,一个文件被保存到文件系统中,一个警报被显示在屏幕上。因此,你可以有多个独立的代码片断,它们都是基于一个事件公告而被付诸行动的。这在C#中是如何实现的呢?通过Delegate!


C#中的代理

在C#中,代理是有趣的事情开始发生的地方。如果你熟悉JavaScript的工作方式,就会很容易想到Delegates。在JavaScript中,你可以将一个函数分配给一个变量。然后,你可以像调用一个函数一样调用这个变量的名字。在C#中,你可以通过使用Delegates来做同样类型的事情。变量本身持有程序中的可执行代码。要在C#中起诉一个Delegate,你首先要创建一个Delegate类型。创建一个Delegate类型与我们使用class关键字或struct关键字来创建一个类型没有什么不同。当创建一个Delegate时,你将使用,你猜对了,就是 **delegate**关键字进行类型定义。

我们将在我们的程序中添加一个委托,所以我们可以先在项目中添加一个类文件,并将其命名为委托。这个委托代表在程序中的投资组合名称发生变化的任何时候。
PortfolioNameChangedDelegate.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks
{
    public delegate void PortfolioNameChangedDelegate(string oldName, string newName);
}

接下来,我们可以在StockPortfolio类中添加一个公共字段,像这样引用我们的新委托。

using System;
using System.Collections.Generic;

namespace Stocks
{
    public class StockPortfolio
    {
        public StockPortfolio()
        {
            _name = "'New Portfolio'";
            stocks = new List<float>();
        }

        public StockStatistics ComputeStatistics()
        {
            StockStatistics stats = new StockStatistics();

            float sum = 0;
            foreach (float stock in stocks)
            {
                stats.HighestStock = Math.Max(stock, stats.HighestStock);
                stats.LowestStock = Math.Min(stock, stats.LowestStock);
                sum += stock;
            }

            stats.AverageStock = sum / stocks.Count;
            return stats;
        }

        public void AddStock(float stock)
        {
            stocks.Add(stock);
        }

        public string Name
        {
            get { return _name; }
            set
            {
                if (!String.IsNullOrEmpty(value))
                {
                    // add code here next
                }
            }
        }

        public PortfolioNameChangedDelegate PortfolioNameChanged;

        private string _name;
        private List<float> stocks;
    }
}

这意味着我们现在可以在我们的类中使用该委托了。因此,在我们的案例中,我们要做的是在StockPortfolio的名称发生变化时发出公告。这意味着我们可以深入研究Name属性,特别是setter。只要_name变量被设置,我们就可以发布一个公告。这里是一个开始。

using System;
using System.Collections.Generic;

namespace Stocks
{
    public class StockPortfolio
    {
        public StockPortfolio()
        {
            _name = "'Starting Portfolio Name'";
            stocks = new List<float>();
        }

        public StockStatistics ComputeStatistics()
        {
            StockStatistics stats = new StockStatistics();

            float sum = 0;
            foreach (float stock in stocks)
            {
                stats.HighestStock = Math.Max(stock, stats.HighestStock);
                stats.LowestStock = Math.Min(stock, stats.LowestStock);
                sum += stock;
            }

            stats.AverageStock = sum / stocks.Count;
            return stats;
        }

        public void AddStock(float stock)
        {
            stocks.Add(stock);
        }

        public string Name
        {
            get { return _name; }
            set
            {
                if (!String.IsNullOrEmpty(value))
                {
                    if (_name != value)
                    {
                        // make announcement
                        PortfolioNameChanged(_name, value);

                        // set the new value
                        _name = value;
                    }
                }
            }
        }

        public PortfolioNameChangedDelegate PortfolioNameChanged;

        private string _name;
        private List<float> stocks;
    }
}

现在,在主Program.cs类中,我们可以将我们的委托分配给我们刚刚添加到StockPortfolio类中的PortfolioNameChanged公共成员。这一行代码看起来是这样的。

portfolio.PortfolioNameChanged = new PortfolioNameChangedDelegate();

在这一点上,Portfolio.Name被设置,然后委托人发出一个公告。在这一点上,我们想让订阅者对该公告采取一个行动。这要怎么做呢?我们需要创建一个新的方法,可以传递给PortfolioNameChangedDelegate()。这样想吧。WhenThisHappens(TakeThisAction); 所以考虑到这一点,我们将把代码更新为这样。

portfolio.PortfolioNameChanged = new PortfolioNameChangedDelegate(OnPortfolioNameChanged);

OnPortfolioNameChanged()方法还不存在,所以我们可以创建它。

static void OnPortfolioNameChanged(string oldName, string newName)
{
    Console.WriteLine($"Portfolio name changed from {oldName} to {newName}! \n");
}

我们现在可以运行该程序,看看会发生什么。看起来它正在工作。当投资组合的名称发生变化时,会有一条信息写到控制台,说明了这一点。
C# delegate example


多播代表

这很不错,但现在我们想提高水平。名字变了,发出了公告,发生了一些事情。目前是一对一的那种。然而,记得我们说过,你可以让一件事发生,然后让多件事发生来回应它。如果当我们调用PortfolioNameChanged()时,我们想触发两个、三个、甚至十个不同的方法呢?你可以这样做!让我们看看这个动作。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks
{
    class Program
    {
        static void Main(string[] args)
        {
            StockPortfolio portfolio = new StockPortfolio();

            portfolio.PortfolioNameChanged += new PortfolioNameChangedDelegate(OnPortfolioNameChanged);
            portfolio.PortfolioNameChanged += new PortfolioNameChangedDelegate(OnPortfolioNameChangedSecondAction);

            portfolio.Name = "'New Portfolio Name'";

            portfolio.AddStock(55.81f);
            portfolio.AddStock(74.52f);
            portfolio.AddStock(92.88f);

            StockStatistics stats = portfolio.ComputeStatistics();
            Console.WriteLine("The Current Name Is: " + portfolio.Name + "\n");
            CustomWriteLine("The Average Stock Price Is: ", stats.AverageStock);
            CustomWriteLine("The Highest Stock Price Is: ", stats.HighestStock);
            CustomWriteLine("The Lowest Stock Price Is: ", stats.LowestStock);
        }

        static void OnPortfolioNameChanged(string oldName, string newName)
        {
            Console.WriteLine($"Portfolio name changed from {oldName} to {newName}! \n");
        }

        static void OnPortfolioNameChangedSecondAction(string oldName, string newName)
        {
            Console.WriteLine("OnPortfolioNameChanged ran, now OnPortfolioNameChangedSecondAction ran \n");
        }

        static void CustomWriteLine(string description, float result)
        {
            Console.WriteLine($"{description}: {result:C} \n", description, result);
        }
    }
}

现在,当程序运行时,如果投资组合名称发生变化,会发生两个动作。首先,OnPortfolioNameChanged()方法触发并向屏幕写出 "投资组合名称从'起始投资组合名称'变为'新投资组合名称'!"。然后,OnPortfolioNameChangedSecondAction()触发并向屏幕写出 "OnPortfolioNameChanged运行,现在OnPortfolioNameChangedSecondAction运行"。
C sharp multicast delegate example

非常好!现在我们可以看到如何在一个事件发生后使许多事情发生。


从委托到事件

通常情况下,在遵循pub-sub原则的情况下,使用 **event**关键字更有意义,因为在遵循pub-sub的编程风格时。其原因是暴露原始的委托是比较容易出错的。使用事件可以增加一些保护,这样在StockPortfolio类之外,其他代码可以做的唯一事情就是为这个事件增加一个订阅者或删除这个事件的订阅者。如果我们只使用委托,就有可能进行分配,这将使所有其他的订阅无效,我们可能不希望这样。所以我们需要做的就是像这样添加事件关键字。

public event PortfolioNameChangedDelegate PortfolioNameChanged;

事件约定

我们想对我们在应用程序中的事件设置方式再做一个改变。我们不应该传递两个字符串PortfolioNameChangedDelegate(),而应该遵循一个流行的惯例,将事件的发送者作为一个参数。如果StockPortfolio类发布了一个名称已经改变的公告,它应该把自己作为第一个参数发送。第二个参数也将是一个包含事件参数的对象。要做到这一点,我们首先要创建一个新的类,像这样。
PortfolioNameChangedEventArgs.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks
{
    public class PortfolioNameChangedEventArgs : EventArgs
    {
        public string oldName { get; set; }
        public string newName { get; set; }
    }
}

一旦该类创建完毕,我们需要更新PortfolioNameChangedDelegate(),以接受发送者和args对象,而不是像之前那样的两个字符串。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks
{
    public delegate void PortfolioNameChangedDelegate(object sender, PortfolioNameChangedEventArgs args);
}

现在,StockPortfolio.cs将需要更新以反映这一变化,就像这样。

using System;
using System.Collections.Generic;

namespace Stocks
{
    public class StockPortfolio
    {
        public StockPortfolio()
        {
            _name = "'Starting Name'";
            stocks = new List<float>();
        }

        public StockStatistics ComputeStatistics()
        {
            StockStatistics stats = new StockStatistics();

            float sum = 0;
            foreach (float stock in stocks)
            {
                stats.HighestStock = Math.Max(stock, stats.HighestStock);
                stats.LowestStock = Math.Min(stock, stats.LowestStock);
                sum += stock;
            }

            stats.AverageStock = sum / stocks.Count;
            return stats;
        }

        public void AddStock(float stock)
        {
            stocks.Add(stock);
        }

        public string Name
        {
            get { return _name; }
            set
            {
                if (!String.IsNullOrEmpty(value))
                {
                    if (_name != value)
                    {
                        PortfolioNameChangedEventArgs args = new PortfolioNameChangedEventArgs();
                        args.oldName = _name;
                        args.newName = value;
                        PortfolioNameChanged(this, args);
                    }
                    _name = value;
                }
            }
        }

        public event PortfolioNameChangedDelegate PortfolioNameChanged;

        private string _name;
        private List<float> stocks;
    }
}

我们需要做的最后一件事是更新Program.cs文件,以反映我们现在使用这两个对象作为参数,而不是像以前那样使用两个字符串。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Stocks
{
    class Program
    {
        static void Main(string[] args)
        {
            StockPortfolio portfolio = new StockPortfolio();

            portfolio.PortfolioNameChanged += OnPortfolioNameChanged;

            portfolio.Name = "'Warren Buffet Portfolio'";
            portfolio.Name = "'Bootleg Capital'";

            portfolio.AddStock(55.81f);
            portfolio.AddStock(74.52f);
            portfolio.AddStock(92.88f);

            StockStatistics stats = portfolio.ComputeStatistics();
            Console.WriteLine("The Current Name Is: " + portfolio.Name + "\n");
            CustomWriteLine("The Average Stock Price Is: ", stats.AverageStock);
            CustomWriteLine("The Highest Stock Price Is: ", stats.HighestStock);
            CustomWriteLine("The Lowest Stock Price Is: ", stats.LowestStock);
        }

        static void OnPortfolioNameChanged(object sender, PortfolioNameChangedEventArgs args)
        {
            Console.WriteLine($"Portfolio name changed from {args.oldName} to {args.newName}! \n");
        }

        static void CustomWriteLine(string description, float result)
        {
            Console.WriteLine($"{description}: {result:C} \n", description, result);
        }
    }
}

现在运行该程序会给我们一些有趣的输出。当程序启动时,名称被初始化为 "起始名称"。然后,当我们指定一个新的名字时,我们看到我们的事件发生了,并且出现了 "投资组合的名字从'起始名字'改为'沃伦-巴菲特投资组合'!"的信息。我们再次改变名称,事件再次触发,给我们一个新的消息:"投资组合名称从'Warren Buffet投资组合'改为'Bootleg Capital'!"。
C# event object sender convention


C#方法和属性总结

在这个C#方法和类属性教程中,我们学到了很多关于可以成为类一部分的不同成员。我们看到,属性和字段是用来在一个对象中存储状态或数据的。当然,方法是用来执行动作的,而且可以重载。换句话说,两个方法看起来是完全一样的,但根据它们接受的参数的数量和类型,它们是不同的。这被称为方法签名,对C#编译器来说,它是一个方法的唯一标识。尽管方法名称有时看起来像是重复的,但基于方法签名,每个方法在.NET中都是独一无二的。