C# 中类的基本概念

一、类的概述

在上一篇文章中(https://www.dotnetprimer.com/csharp/01-types-and-variables-in-csharp/#六用户定义类型),我们看到 C# 提供了 6 种用户定义类型。其中最重要的,也是首先要阐述的是。因为类在 C# 中是个很大的主题,所以关于它的讨论将会延伸到接下来的几篇文章。

类是一种活动的数据结构

在面向对象的分析和设计出现之前,程序员们仅把程序当作指令的序列,那时的焦点主要放在指令的组合和优化上。随着面向对象的出现,焦点从优化指令转移到组织程序的数据和功能上来。程序的数据和功能被组织为逻辑上相关的数据项和函数的封装集合,并被称为

类是一个能存储数据并执行代码的数据结构。它包含数据成员和函数成员。

  • 数据成员 它存储与类或类的实例相关的数据。数据成员通常模拟该类所表示的现实世界事物的特性。
  • 函数成员 它执行代码,通常会模拟类所表示的现实世界事物的功能和操作。

一个 C# 类可以有任意数目的数据成员和函数成员。成员可以是 9 种成员类型的任意组合。这些成员类型如下表所示。

数据成员存储数据 函数成员执行代码
字段
常量
方法                运算符
属性                索引器
构造函数        事件
析构函数

说明 类是逻辑相关的数据和函数的封装,通常代表真实世界中或概念上的事物。

二、程序和类:一个简单的示例

一个运行中的 C# 程序是一组相互作用的类型对象,它们中的大部分是类的实例。例如,假设有一个模拟扑克牌游戏的程序。当程序运行时,它有一个名为 Dealer 的类实例,它的工作就是运行游戏。还有几个名为 Player 的类实例,它们代表游戏的玩家。

Dealer 对象保存纸牌的当前状态和玩家数目等信息。它的动作包括洗牌和发牌。

Player 类有很大不同。它保存玩家名称以及用于押注的钱等信息,并实现如分析玩家当前手上的牌和出牌这样的动作。运行中的程序如下图所示。类名显示在方框外面,实例名显示在方框内。

一个真正的程序无疑会包含除 DeaLerPlayer 之外的许多具他的类,还会包括像 CardDeck 这样的类。每一个类都模拟某种扑克牌游戏中的事物

说明 运行中的程序是一组相互作用的对象的集合。

三、声明类

或许你能猜到,虽然类型 intdoublechar 由 C# 定义,但像 DealerPlayer 这样的类不是由语言定义的。如果想在程序中使用它们,你必须通过编写类的声明自己定义它们。

类的声明定义新类的特征和成员。它并不创建类的实例,但创建用于创建实例的模板。类的声明提供下列内容:

  • 类的名称;
  • 类的成员;
  • 类的特征。

下面是一个最简单的类声明语法示例。大括号内包含了成员的声明,它们组成了类主体。类成员可以在类主体内部以任何顺序声明。这意味着一个成员的声明完全可以引用另一个在后面的类声明中才定义的成员。

下面的代码给出了两个类声明的概貌:

class Dealer    // 类声明
{
    ...
}

class Player    // 类声明
{
    ...
}

说明 因为类声明“定义”了一个新类,所以经常会在文献和程序员的日常使用中看到类声明被称为类定义

四、类成员

字段和方法是最重要的类成员类型。字段是数据成员,方法是函数成员。

4.1 字段

字段是隶属于类的变量。

  • 它可以是任何类型,无论是预定义类型还是用户定义类型。
  • 和所有变量一样,字段用来保存数据,并具有如下特征:
    • 可以被写入;
    • 可以被读取。

声明一个字段最简单的语句如下:

例如,下面的类包含字段 MyField 的声明,它可以保存 int 值:

class MyClass
{
    int MyField;
}

说明 与 C 和 C++ 不同,C# 在类型的外部不能声明全局变量(也就是变量或字段)。所有的字段都属于类型,而且必须在类型声明内部声明。

1. 显式和隐式字段初始化

因为字段是一种变量,所以字段初始化语句在语法上和上一篇([{{< ref "01-04-类型和变量.md#91-变量声明" >}}]({{< ref "01-04-类型和变量.md#91-变量声明" >}}))所述的变量初始化语句相同。

  • 字段初始化语句是字段声明的一部分,由一个等号后面跟着一个求值表达式组成。

  • 初始化值必须是编译时可确定的。

    class MyClass
    {
        int f1 = 17;
    }
    
  • 如果没有初始化语句,字段的值会被编译器设为默认值,默认值由字段的类型决定。简单类型的默认值见([{{< ref "01-04-类型和变量.md#五预定义类型" >}}]({{< ref "01-04-类型和变量.md#五预定义类型" >}}))。可是总结起来,每种值类型的默认值都是 0,bool 型的默认值是 false,引用类型的默认值为 null

例如,下面的代码声明了 4 个字段,前面两个字段被隐式初始化,另外两个字段被初始化语句显示初始化。

class MyClass
{
    int f1;               // 初始化为 0     - 值类型
    string f2;            // 初始化为 null  - 引用类型

    int f3 = 25;          // 初始化为 25
    string f4 = "abcd";   // 初始化为 “abcd”
}

2. 声明多个字段

可以通过用逗号分隔名称的方式,在同一条语句中声明多个相同类型的字段。但不能在一个声明中混合不同的类型。例如,可以把之前的 4 个字段声明结合成两条语句,语义结果相同。

int f1,f3 = 25;
string f2,f4 = "abcd";

4.2 方法

方法是具有名称的可执行代码块,可以从程序的很多不同地方执行,甚至从其他程序中执行。

当方法被调用(call/invoke)时,它执行自己所含的代码,然后返回到调用它的代码并继续执行调用代码。有些方法返回一个值到它们被调用的位置。方法相当于 C++ 中的成员函数

声明方法的最简语法包括以下组成部分。

  • 返回类型 它声明了方法返回值的类型。如果一个方法不返回值,那么返回类型被指定为 void
  • 名称 这是方法的名称。
  • 参数列表 它至少由一对空的圆括号组成。如果有参数([{{< ref "01-06-方法和参数.md" >}}]({{< ref "01-06-方法和参数.md" >}})),将被列在圆括号中间。
  • 方法体 它由一对大括号组成,大括号内包含执行代码。

例如,下面的代码声明了一个类,带有一个名为 PrintNums 的简单方法。从这个声明中可以看出下面几点关于 PrintNums 的情况:

  • 它不返回值,因此返回类型指定为 void
  • 它有空的参数列表;
  • 它的方法体有两行代码,其中第 1 行打印数字 1,第 2 行打印数字 2。
class SimpleClass
{
    void PrintNums()
    {
        Console.WriteLine("1");
        Console.WriteLine("2");
    }
}

说明与 C 和 C++ 不同,C# 中没有全局函数(也就是方法或函数)声明在类型声明的外部。同样,和 C/C++ 不同,C# 中方法没有默认的返回类型。所有方法必须包含返回类型或 void

五、创建变量和类的实例

类的声明只是用于创建类的实例的蓝图。一旦类被声明,就可以创建类的实例。

  • 类是引用类型,正如你从上一篇([{{< ref "01-04-类型和变量.md#八值类型和引用类型" >}}]({{< ref "01-04-类型和变量.md#八值类型和引用类型" >}}))了解到的,这意味着它们要为数据引用和实际数据都申请内存。
  • 数据的引用保存在一个类类型的变量中。所以,要创建类的实例,需要从声明一个类类型的变量开始。如果变量没有被初始化,它的值是未定义的。

下图阐明了如何定义保存引用的变量。左边顶端的代码是类 Dealer 的声明,下面是类 Program 的声明,它包含 Main 方法。Main 声明了 Dealer 类型的变量 theDealer。因为该变量没有初始化,所以它的值是未定义的。

六、为数据分配内存

声明类类型的变量所分配的内存是用来保存引用的,而不是用来保存类对象实际数据的。要为实际数据分配内存,需要使用 new 运算符。

  • new 运算符为任意指定类型的实例分配并初始化内存。它依据类型的不同,从栈或堆里分配。

  • 使用 new 运算符组成的一个对象创建表达式,它的组成如下:

    • 关键字 new;

    • 要分配内存的实例的类型名称;

    • 成对的括号,可能包括参数也可能没有参数。

  • 如果将内存分配给一个引用类型,则对象创建表达式返回一个引用,指向在堆中被分配并初始化的对象实例。

要分配和初始化用于保存类实例数据的内存,需要的工作就是这些。下面是使用 new 运算符创建对象创建表达式,并把它的返回值赋给类变量的一个例子:

Dealer theDealer;            // 声明引用变量
theDealer = new Dealer();    // 为类对象分配内存并赋值给变量

下图左边的代码展示了用干分配内存并创建类 Dealer 实例的 new 云算符,随后实例被赋值给类变量。右边的图展示了内存的结构。

合并这两个步骤

可以将这两个步骤合并起来,用对象创建表达式来初始化变量。

Dealer theDealer = new Dealer();    // 声明并初始化

七、实例成员

类声明相当于蓝图,通过这个蓝图想创建多少个类的实例都可以。

  • 实例成员 类的每个实例都是不同的实体,它们有自己的一组数据成员,不同于同一类的其他实例。因为这些数据成员都和类的实例相关,所以被称为实例成员
  • 静态成员 实例成员是默认类型,但也可以声明与类而不是实例相关的成员,称为静态成员

下面的代码是实例成员的示例,展示了有 3 个 Player 类实例的扑克牌程序。下图表明每个实例的 Name 字段都有不同的值。

class Dealer {...}    // 声明类
class Player          // 声明类
{
    string Name;      // 字段
        ...
}

class Program
{
    static void Main()
    {
        Dealer theDealer = new Dealer();
        Player player1 = new Player();
        Player player2 = new Player();
        Player player3 = new Player();
        ...
    }
}

实例成员在类对象之间的值是不同的

实例成员在类对象之间的值是不同的

八、访问修饰符

从类的内部,任何函数成员都可以使用成员的名称访问类中任意的其他成员。

访问修饰符是成员声明的可选部分,指明程序的其他部分如何访问成员。访问修饰符放在简单声明形式之前。下面是字段和方法声明的语法:

字段

访问修饰符 类型 标识符;

方法

访问修饰符 返回类型 方法名()
{
    ...
}

5 种成员访问控制如下。本文将阐述前两种。

  • 私有的(private);
  • 公有的(public);
  • 受保护的(protected);
  • 内部的(internal);
  • 受保护内部的(protected internal)。

私有访问和公有访问

私有成员只能从声明它的类的内部访问,其他的类看不见或无法访问它们。

  • 私有访问是默认的访问级别,所以,如果一个成员在声明时不带访问修饰符,那它就是私有成员。
  • 还可以使用 private 访问修饰符显式地将一个成员声明为私有。隐式地声明私有成员和显式地声明在语义上没有不同,两种形式是等价的。

例如,下面的两个声明都指定了 private int 成员:

int MyInt1;            // 隐式声明为私有
private int MyInt2;    // 显示声明为私有

实例的公有成员可以被程序中的其他对象访问。必须使用 public 访问修饰符指定公有访问。

public int MyInt;

1. 公有访问和私有访问图示

本文中的插图把类表示为标签框,如下图所示。

  • 类成员为类框中的小标签框。
  • 私有成员完全封闭在它们的类框中。
  • 公有成员有一部分伸出它们的类框之外。

2. 成员访问示例

C1 声明了公有和私有的字段和方法,下图阐明了类 C1 的成员的可见性。

class C1
{
    int F1;               // 隐式私有字段
    private int F2;       // 显示私有字段
    public int F3;        // 公有字段

    void DoCalc()         // 隐式私有方法
    {
        ...
    }

    public int GetVal()    // 公有方法
    {
        ...
    }
}

类的私有成员和公有成员

类的私有成员和公有成员

九、从类的内部访问成员

如前所述,类的成员仅用其他类成员的名称就可以访问它们。

例如,下面的类声明展示了类的方法对字段和其他方法的访问。即时字段和两个方法被声明为 private,类的所有成员还是可以被类的任何方法(或任何函数成员)访问。下图阐明了这段代码。

class DaysTemp
{
    // 字段
    private int High = 75;
    private int Low = 45;

    // 方法
    private int GetHigh()
    {
        return High;  // 访问私有字段
    }

    private int GetLow()
    {
        return Low;  // 访问私有字段
    }

    public float Average()
    {
        return (GetHigh() + GetLow()) / 2;  //  访问私有方法
    }

}

类内部的成员可以自由地互相访问

类内部的成员可以自由地互相访问

十、从类的外部访问成员

要从类的外部访问实例成员,必须包括变量名称和成员名称,中间用句点(.)分隔。这称为点运算符(dot-syntax notation),后文会详细讨论。

例如,下面代码的第二行展示了一个从类的外部访问方法的示例:

DaysTemp myDt = new DaysTemp();  // 创建类的对象
float fValue = myDt.Average();   // 从外部访问

举个例子,下面的代码声明了两个类:DaysTempProgram

  • DaysTemp 内的两个字段被声明为 public,所以可以从类的外部访问它们。
  • 方法 Main 是类 Program 的成员。它创建了一个变量和类 DaysTemp 的对象,并给对象的字段赋值。然后它读取字段的值并打印出来。
class DaysTemp  // 声明类 DaysTemp
{
    public int High = 75;
    public int Low = 45;
}

class Program  // 声明类 Program
{
    static void Main()
    {
        DaysTemp temp = new DaysTemp();  // 创建对象
        temp.High = 85;  // 字段赋值
        temp.Low = 60;

        Console.WriteLine($"High: {temp.High}"); // 读取字段值
        Console.WriteLine($"Low: {temp.Low}");
    }
}

输出:

High: 85
Low: 60

十一、综合应用

下面的代码创建两个实例并把它们的引用保存在名称为 t1t2 的变量中。下图阐明了内存中的 t1t2。这段代码示范了目前为止讨论的使用类的 3 种行为:

  • 声明一个类;
  • 创建类的实例;
  • 访问类的成员(也就是写入字段和读取字段)。
class DaysTemp  // 声明类
{
    public int High,Low;  // 声明实例字段

    public int Average()
    {
        return (High + Low) / 2;
    }
}

class Program
{
    static void Main()
    {
        // 创建两个 DaysTemp 实例
        DaysTemp t1 = new DaysTemp();
        DaysTemp t2 = new DaysTemp();

        // 给字段赋值
        t1.High = 76;
        t1.Low = 57;

        t2.High = 75;
        t2.Low = 53;

        // 读取字段值
        // 调用实例的方法

        Console.WriteLine($"t1: {t1.High},{t1.Low},{t1.Average()}");
        Console.WriteLine($"t2: {t2.High},{t2.Low},{t2.Average()}");
    }
}

输出:

t1: 76,57,66
t2: 75,53,64

实例 t1 和 t2 的内存布局

实例 t1 和 t2 的内存布局

原文链接:https://www.dotnetprimer.com/csharp/01-classes-in-csharp

(完)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


项目中经常遇到CSV文件的读写需求,其中的难点主要是CSV文件的解析。本文会介绍CsvHelper、TextFieldParser、正则表达式三种解析CSV文件的方法,顺带也会介绍一下CSV文件的写方法。 CSV文件标准 在介绍CSV文件的读写方法前,我们需要了解一下CSV文件的格式。 文件示例 一
简介 本文的初衷是希望帮助那些有其它平台视觉算法开发经验的人能快速转入Halcon平台下,通过文中的示例开发者能快速了解一个Halcon项目开发的基本步骤,让开发者能把精力完全集中到算法的开发上面。 首先,你需要安装Halcon,HALCON 18.11.0.1的安装包会放在文章末尾。安装包分开发和
这篇文章主要简单记录一下C#项目的dll文件管理方法,以便后期使用。 设置dll路径 参考C#开发奇技淫巧三:把dll放在不同的目录让你的程序更整洁中间的 方法一:配置App.config文件的privatePath : &lt;runtime&gt; &lt;assemblyBinding xml
在C#中的使用JSON序列化及反序列化时,推荐使用Json.NET——NET的流行高性能JSON框架,当然也可以使用.NET自带的 System.Text.Json(.NET5)、DataContractJsonSerializer、JavaScriptSerializer(不推荐)。
事件总线是对发布-订阅模式的一种实现,是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。&#xA;EventBus维护一个事件的字典,发布者、订阅者在事件总线中获取事件实例并执行发布、订阅操作,事件实例负责维护、执行事件处理程序。
通用翻译API的HTTPS 地址为https://fanyi-api.baidu.com/api/trans/vip/translate,使用方法参考通用翻译API接入文档 。&#xA;请求方式可使用 GET 或 POST 方式(Content-Type 请指定为:application/x-www-for
词云”由美国西北大学新闻学副教授、新媒体专业主任里奇·戈登(Rich Gordon)于2006年最先使用,是通过形成“关键词云层”或“关键词渲染”,对文本中出现频率较高的“关键词”的视觉上的突出。词云图过滤掉大量的文本信息,使浏览者只要一眼扫过文本就可以领略文本的主旨。&#xA;网上大部分文章介绍的是使用P
微软在.NET中对串口通讯进行了封装,我们可以在.net2.0及以上版本开发时直接使用SerialPort类对串口进行读写操作。&#xA;为操作方便,本文对SerialPort类做了一些封装,暂时取名为**SerialPortClient**。
简介 管道为进程间通信提供了平台, 管道分为两种类型:匿名管道、命名管道,具体内容参考.NET 中的管道操作。简单来说,匿名管道只能用于本机的父子进程或线程之间,命名管道可用于远程主机或本地的任意两个进程,本文主要介绍命名管道的用法。 匿名管道在本地计算机上提供进程间通信。 与命名管道相比,虽然匿名
目录自定义日志类NLog版本的日志类Serilog版本的日志类 上个月换工作,新项目又要重新搭建基础框架,把日志实现部分单独记录下来方便以后参考。 自定义日志类 代码大部分使用ChatGPT生成,人工进行了测试和优化,主要特点: 线程安全,日志异步写入文件不影响业务逻辑 支持过期文件自动清理,也可自
[TOC] # 原理简介 本文参考[C#/WPF/WinForm/程序实现软件开机自动启动的两种常用方法](https://blog.csdn.net/weixin_42288432/article/details/120059296),将里面中的第一种方法做了封装成**AutoStart**类,使
简介 FTP是FileTransferProtocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于Internet上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的FTP应用程序,而所有这些应用程序都遵守同一种协议以传输文件。 FTP
使用特性,可以有效地将元数据或声明性信息与代码(程序集、类型、方法、属性等)相关联。 将特性与程序实体相关联后,可以在运行时使用反射这项技术查询特性。&#xA;在 C# 中,通过用方括号 ([]) 将特性名称括起来,并置于应用该特性的实体的声明上方以指定特性。
# 简介 主流的识别库主要有ZXing.NET和ZBar,OpenCV 4.0后加入了QR码检测和解码功能。本文使用的是ZBar,同等条件下ZBar识别率更高,图片和部分代码参考[在C#中使用ZBar识别条形码](https://www.cnblogs.com/w2206/p/7755656.htm
C#中Description特性主要用于枚举和属性,方法比较简单,记录一下以便后期使用。 扩展类DescriptionExtension代码如下: using System; using System.ComponentModel; using System.Reflection; /// &lt;
本文实现一个简单的配置类,原理比较简单,适用于一些小型项目。主要实现以下功能:保存配置到json文件、从文件或实例加载配置类的属性值、数据绑定到界面控件。&#xA;一般情况下,项目都会提供配置的设置界面,很少手动更改配置文件,所以选择以json文件保存配置数据。
前几天用SerialPort类写一个串口的测试程序,关闭串口的时候会让界面卡死。网上大多数方法都是定义2个bool类型的标记Listening和Closing,关闭串口和接受数据前先判断一下。我的方法是DataReceived事件处理程序用this.BeginInvoke()更新界面,不等待UI线程
约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。 编译器只能假定 System.Object 的成员,它是任何 .NET 类型的最终基类。 如果客户端代码使用不满足约束的类型,编译器将发出错误。 通过使用 where 上下文关键字指定约束。&#xA;最常用的泛型约束为
protobuf-net是用于.NET代码的基于契约的序列化程序,它以Google设计的“protocol buffers”序列化格式写入数据,适用于大多数编写标准类型并可以使用属性的.NET语言。&#xA;protobuf-net可通过NuGet安装程序包,也可直接访问github下载源码:https:/
工作中经常遇到需要实现TCP客户端或服务端的时候,如果每次都自己写会很麻烦且无聊,使用SuperSocket库又太大了。这时候就可以使用SimpleTCP了,当然仅限于C#语言。&#xA;SimpleTCP是一个简单且非常有用的 .NET 库,用于处理启动和使用 TCP 套接字(客户端和服务器)的重复性任务