【C#本质论 六】类-从设计的角度去认知(封装)

从这一章开始,基本就脱离了结构化编程的思维,所以我的博客排版方式也会摒弃之前的节选方式,而是深入的去理解和描述内容,而这本书给我带来的惊喜就是:能够从设计的角度去帮助我理解为何类要如此设计,其设计缘由是什么,我觉得这一点远比单纯的告诉你怎么用更重要!感觉这一章的干货特别多,可以让我的面向对象思维上一个小小的台阶,这里将自己的学习笔记以及自己的理解整理分享出来,希望对大家都有所帮助,还有一点要说明下:面向对象思维远比语言的选择更重要,所以无论Java、C++还是C#,不同的语法只是为了满足自己特定场合的用途,而核心思想才是最重要的,实现方式反而没那么需要重视(R语言也一样哦,你想学,我就提前学学教你hhh)!

在这里插入图片描述

宏观认知

有别于作者的排列方式,我想先宏观的串讲下概念,然后再填充细节。采用连续发问的方式来引发你的思考和明白没一项的场景需求:

1,为什么要有类?

相较于之前5章对面向过程编程层面的理解,我觉得面向对象编程的好处就是:可以不必从头创建新程序,而是用现有的一个模板去复制、去扩展,或添加更多,而且整个代码还非常有条理,可以控制访问权限、组织起来不会乱,还有就是理解起来也更加容易,而类就是用来完成面向对象的使命的

2,面向对象的三大特性:继承、封装、多态?

依据面向对象编程的好处,我们可以确定,面向对象比面向过程的优势:可扩展、好组织、能控制访问权限。所以对应的类一定要具有这样的能力:封装类的细节、派生类型来扩展基类型的能力(包括数据和方法)、方法或类型的多态化实现

3,如何实现封装这一特性(本篇只介绍封装部分,下一章介绍继承和多态)?

这就需要很好的组织类的成员,先来了解一下:实例字段、静态字段、实例方法、静态方法、普通构造函数(实例)、静态构造函数、嵌套类。用访问修饰符来限定以上内容的访问方式,当然,如果限定了private的字段,我【实例.字段】是看不到私有成员的,但是我又想给该字段赋值,或者想让数据在外部只读,而能内部修改,简而言之就是想让字段能具有更加细粒度的操作内容,怎么办呢,传统的方式是使用public的get和set方法,但是C#提供了属性来简化这一实现。

4,既然我都有了实例字段、实例方法,我为啥还需要静态的字段和方法?书上有个例子举的特别好:

类和对象都能关联数据。将类想象成模具,将对象想象成根据该模具浇铸的零件,可以更好地理解这一点。例如,一个模具拥有的数据可能包括:到目前为止已用模具浇铸的零件数、下个零件的序列号、当前注入模具的液态塑料的颜色以及模具每小时生产零件数量。类似地,零件也拥有它自己的数据:序列号、颜色以及生产日期/时间。虽然零件颜色就是生产零件时在模具中注入的塑料的颜色,但它显然不包含模具中当前注入的塑料颜色数据,也不包含要生产的下个零件的序列号数据。

设计对象时,程序员要考虑字段和方法应声明为静态还是基于实例。一般应将不需要访问任何实例数据的方法声明为静态方法,将需要访问实例数据的方法(实例不作为参数传递)声明为实例方法。静态字段主要存储对应于类的数据,比如新实例的默认值或者已创建实例个数。而实例字段主要存储和对象关联的数据

5,扩展方法有啥意义?

其实我感觉没啥意义,就是如果一个已经写好的类缺少你比较需要的一个功能,可以通过扩展方法来创造一个扩展类出来,然后在里面写一个扩展方法,这样你的类的对象就可以直接调用该扩展方法了,就好像你的类本身就具有这样的方法一样。其实我感觉这种方式用的应该比较少,如果对象想实现一个类没有的方法,完全可以搞个派生类出来

6,嵌套类有啥用?

如果一个类在它的包容类外部没有多大意义,就适合设计成嵌套类,换言之,这个类对于它的包容类比较专属,通过嵌套的方式可以限定它不被滥用。

7,为啥要有分部类和分部方法

我这么理解:其实说白了就是,一个类或方法如果在修改的时候有一部分老是不变,就把它单独抽成一个文件呗,避免反复无意义的重新生成,而且有时候如果多个程序员同时改一个类,虽然有git版本控制,但是拆开来岂不是更好。

对象和类

类的声明和实例化其实已经不必赘述了,需要注意的就是以下两点规范:

  1. 不要在一个源代码文件中放多个类
  2. 要用所含公共类型的名称命名源代码文件(如果你非要不听话的在源代码文件中放多个类)

而对象就是类new出来的,将数据和方法(行为)组合在一起。而访问修饰符可以限定其访问范围:
在这里插入图片描述
各种类型可使用的访问修饰符
在这里插入图片描述需要注意的是,这表明的是可见性!就是你能通过智能感知器关联看到!还有就是成员默认私有!如果不为类的成员添加访问修饰符,默认的访问修饰符为private!

属性的用法

为啥要有属性,就像我在上文提到的,如果我又想保证不可见,有想不通过方法调用来赋值或者取值,使用属性多合适啊!

声明属性

1,简单的属性声明方式

  class Employee
    {
        // FirstName property
        public string FirstName
        {
            get
            {
                return _FirstName;
            }
            set
            {
                _FirstName = value;
            }
        }
        private string _FirstName;  //字段

    }

2,花式的使用表达式主题方法的声明方式,C#7.0支持:

    public string LastName
        {
            get => _LastName;
            set => _LastName = value;
        }
        private string _LastName;

3,高端玩儿法:C#3.0支持自动属性:


        // Title property
        public string Title { get; set; }

        // Manager property
        public Employee Manager { get; set; }

        public string Salary { get; set; } = "Not Enough";  //C#6.0支持直接赋值

属性设计规范

属性有如下几条使用规范,最需要注意的就是,属性代表数据,而方法代表行动,不用混为一谈

  1. 要使用属性简化对简单数据的访问(只进行简单计算)。
  2. 避免从属性取值方法抛出异常。
  3. 要在属性抛出异常时保留原始属性值。
  4. 如果不需要额外逻辑,要优先使用自动实现的属性,而不是属性加简单支持字段

当然也有一定的书写和命名规范:

  • 考虑为支持字段和属性使用相同的大小写风格,为支持字段附加“_”前缀。但不要使用双下划线,它是为C#编译器保留的。
  • 要使用名词、名词短语或形容词命名属性。
  • 考虑让属性和它的类型同名。
  • 避免用camelCase大小写风格命名字段。
  • 如果有意义的话,要为Boolean属性附加“Is”,“Can”或“Has”前缀。
  • 不要声明public或protected实例字段(而是通过属性公开)
  • 要用PascalCase大小写风格命名属性。
  • 要优先使用自动实现的属性而不是字段。
  • 如果没有额外的实现逻辑,要优先使用自动实现的属性而不是自己写完整版本。

总之优先使用自动实现的属性,使用PascalCase风格命名。

属性验证

属性的一个重要作用就是在设置值或者取值的时候做验证。在验证的时候,关键字value默认为传入参数,不需要单独声明,当然验证的时候自动属性就不好使喽。

 public class Employee
    {
        // ...
        public void Initialize(
            string newFirstName, string newLastName)
        {
            // Use property inside the Employee
            // class as well
            LastName = newLastName;
        }

        // LastName property
        public string LastName
        {
            get => _LastName;
            set
            {
                // Validate LastName assignment
                if(value == null)
                {
                    // Report error
                    throw new ArgumentNullException(nameof(value));
                }
                else
                {
                    // Remove any whitespace around
                    // the new last name
                    value = value.Trim();
                    if(value == "")
                    {
                        throw new ArgumentException(
                            "LastName cannot be blank.", nameof(value));
                    }
                    else
                    {
                        _LastName = value;
                    }
                }
            }
        }
        private string _LastName;    
    }

还有一条重要规范就是:避免从属性外部(即使是属性所在的类,包括构造方法)访问属性的支持字段。也就是字段一切听属性的,保证修改的单一性。这里简单说说nameof关键字:主要作用是方便获取类型、成员和变量的简单字符串名称(非完全限定名),nameof可以用于获取具名表达式的当前名字的简单字符串表示(非完全限定名),注意这个非完全限定:

using static System.Console;
using TML = System.ConsoleColor;
internal class Program
     {
         private static void Main()
          {
            WriteLine(nameof(TML ));
            //输出TML ,因为它是当前的名字,虽然是指向System.ConsoleColor枚举的别名,但是由于TML是当前的名字,那么nameof运算符的结果就是"TML"。
            WriteLine(nameof(System.ConsoleColor));//ConsoleColor
        }
   }

只读或只写的设定

这里以设置只读为例来介绍下几种设定方式:

1,方式一,传统方式,去掉set方法就行了,如果想赋值只能通过构造函数或方法对字段赋值本类构造函数或方法不能修改属性值

 class Employee
    {
        public void Initialize(int id)
        {
            // Use field because Id property has no setter;
            // it is read-only
            _Id = id.ToString();   //属性不能赋值了,只能通过这种方式
        }
        // Id property declaration
        public string Id
        {
            get => _Id;
            // No setter provided  //没有set方法就行
        }
        private string _Id = default(string);
   
    }

2,方式二,只读自动属性方式,如果想赋值,只能通过初始化器或者:构造函数或方法(对字段赋值)。需要注意的是,如果只读的是引用,那么其实可以修改和写入该数组的值。本类构造函数或方法不能修改属性值

 public class Class1
    {
        public int[] Cells { get; } = { 2, 3, 4 };
    }

    public class Class2
    {
        private Class1 ctsClass1 = new Class1();

        public void first()
        {
            ctsClass1.Cells[0] = 5;
            var test = new int[4];
          //  ctsClass1.Cells = test; 
           //报错,因为是只读的,所以其实这里的支持字段是数组类型,只读的其实是引用
        }
    }

3,方式三:get和set级别的访问修饰符,本类构造函数或方法可以修改属性值,实现如下:

 class Program
    {
        static void Main()
        {
            Employee employee1 = new Employee();
            employee1.Initialize(42);
            // ERROR: The property or indexer 'Employee.Id' 
            // cannot be used in this context because the set 
            // accessor is inaccessible
            //employee1.Id = "490";                     //will not compile if you uncomment this line
        }
    }

    class Employee
    {
        public void Initialize(int id)
        {
            // Set Id property
            Id = id.ToString();
        }

        // ...
        // Id property declaration
        public string Id
        {
            get => _Id;
            // Providing an access modifier is possible in C# 2.0
            // and higher only
            private set => _Id = value;  //私有化set
        }
        private string _Id;
    }

属性作为虚字段

属性还能这么玩儿:属性可以不支持字段,也就是当做虚字段玩儿,也就是根本没往内存中写入Name这个字段值属性被当成方法实现了,其实不太建议这么玩儿。

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter06.Listing06_22
{
    using System;
    using System.IO;

    public class Program
    {
        public static void Main()
        {
            Employee employee1 = new Employee();

            employee1.Name = "Inigo Montoya";
            System.Console.WriteLine(employee1.Name);

            // ...
        }
    }

    class Employee
    {
        // FirstName property
        public string FirstName
        {
            get
            {
                return _FirstName;
            }
            set
            {
                _FirstName = value;
            }
        }
        private string _FirstName;

        // LastName property
        public string LastName
        {
            get => _LastName;
            set => _LastName = value;
        }
        private string _LastName;
        // ...

        // Name property
        public string Name
        {
            get
            {
                return $"{ FirstName } { LastName }";
            }
            set
            {
                // Split the assigned value into 
                // first and last names
                string[] names;
                names = value.Split(new char[] { ' ' });
                if(names.Length == 2)
                {
                    FirstName = names[0];
                    LastName = names[1];
                }
                else
                {
                    // Throw an exception if the full 
                    // name was not assigned
                    throw new System.ArgumentException(
                        $"Assigned value '{ value }' is invalid",
                        "value");
                }
            }
        }

        public string Initials => $"{ FirstName[0] } { LastName[0] }";

        // Title property
        public string Title { get; set; }

        // Manager property
        public Employee Manager { get; set; }
    }
}

由此引发的思考就是:为啥不能用ref和out来修饰属性呢,你想啊,如果属性被当成虚字段使,内存中根本没地址,怎么传递地址啊,ref是需要初始化的,你这就相当于没有初始化字段

属性的内部构造

事实上,通过观察IL代码,属性就是一种方法实现,我可以理解其为一种语法糖:
在这里插入图片描述

构造器和终结器

其实构造函数都比较熟悉了,就聊一点儿需要注意的地方:

  1. 构造函数赋值优先于字段声明赋值,所以构造函数的赋值会覆盖字段声明赋值
  2. C#编译器默认支持无参构造函数,一旦显式添加,即使是手动添加无参的,编译器也不会再提供默认的

构造函数可以重载,一个使用规范是:要优先调用可选参数的方法而不是重载

对象初始化器

对象初始化器,其实就是一种语法糖,需要注意的是,初始化时会按顺序赋值

  public static void Main()
        {
            Employee employee = new Employee("Inigo", "Montoya") 
                { Title = "Computer Nerd", Salary = "Not enough" };
        }

同样集合初始化器也一个道理,注意,会按顺序添加哦!

构造函数链

构造函数可以互相调用,避免代码冗余:

 public class Employee
    {
        public Employee(string firstName, string lastName)
        {
            FirstName = firstName;
            LastName = lastName;
        }

        public Employee(
            int id, string firstName, string lastName)
            : this(firstName, lastName)  //实际情况是参数少的调用参数多的
        {
            Id = id;
        }

        public Employee(int id)
        {
            Id = id;
        }

        public int Id { get; private set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Salary { get; set; } = "Not Enough";

        // ...
    }

通常做法是这样的:

 public Employee(string firstName, string lastName):this(firstName, lastName,"add")
        {
            FirstName = firstName;
            LastName = lastName;
        }
        public Employee(string firstName, string lastName,string test=null) 
        {
            FirstName = firstName;
            LastName = lastName;
        }

注意:调用方的参数一定要包含被调用方的全部参数!,例如这样调用:
在这里插入图片描述

集中初始化和解构函数

集中初始化没什么好说的,就是把所有属性参数什么的在一个函数里一次性初始化,通常是构造函数。而解构函数就是通过Deconstruct方法

 public void Deconstruct(
            out int id, out string firstName, 
            out string lastName, out string salary)
        {
           (id, firstName, lastName, salary) = 
                (Id, FirstName, LastName, Salary);
        }

调用的时候:

employee.Deconstruct(out id, out firstName,out lastName, out salary);

C#7.0后支持直接将对象解构为元组,无需方法调用:

(_,firstName,lastName,salary) = employee

静态

静态字段、方法已经在前文说明了,这里简单说一下其特性:

  • 静态字段的作用域为其所属的类。
  • 无论是方法(静态、实例)还是字段(静态、实例),都不能同名,方法重载除外,是说这四种都不能重名!
  • 和普通构造函数一样,静态构造函数赋值优先于静态字段声明!
  • 静态类不包含任何实例成员:包括字段、方法等

有个地方需要注意:最好在声明时进行静态初始化(而不要使用静态构造函数):静态构造函数在首次访问类的任何成员之前执行,无论该成员是静态字段,是其他静态成员,还是实例构造函数。为支持这个设计,编译器添加代码来检查类型的所有静态成员和构造函数,确保首先运行静态构造函数。如果没有静态构造函数,编译器会将所有静态成员初始化为它们的默认值,而且不会添加对静态构造函数的检查。结果是静态字段会在访问前得到初始化,但不一定在调用静态方法或任何实例构造函数之前。有时对静态成员进行初始化的代价比较高,而且访问前确实没必要初始化,所以这个设计能带来一定的性能提升。有鉴于此,请考虑要么以内联方式初始化静态字段(而不要使用静态构造函数),要么在声明时初始化。

局部变量、实例字段和静态字段

局部变量如果使用前没有赋值会报错,静态字段则会使用默认值,实例字段如果在对象生成后也会使用默认值:
在这里插入图片描述

静态方法和实例方法

实例方法可以直接调用静态方法,静态方法不能通过this调用实例方法,只能实例化一个对象,再调用实例方法:

 public static void fecond()
        {
            Class1 ct = new Class1();
            ct.second();
        }

const和readonly

const就是常量的意思,默认为静态字段,而且只限于字面值类型(int,long,string)。此时再声明static反而会报错。而readly也可以用来声明字段,值得注意的是,只能声明字段,而不能声明局部变量!readonly表明字段值只能从构造函数中修改或者初始化器指定,但其实只读自动属性一用,readonly就没啥用了。

扩展方法和特殊类

包括:扩展方法、嵌套类、分部类及分部方法:其实这一部分,我觉得可以用两个字概述:解耦,做的事情无非就是将耦合度降低

扩展方法

扩展方法在上文提到过,感觉实则没有任何用处,需要注意的是,即使不在一个程序集的类也可以添加。

 public class Program
    {
        public static void Main()
        {
            DirectoryInfo directory = new DirectoryInfo(".\\Source");
            directory.CopyTo(".\\Target",
                SearchOption.TopDirectoryOnly, "*");     
        }
    }
    public static class DirectoryInfoExtension
    {
        public static void CopyTo(
            this DirectoryInfo sourceDirectory, string target,
            SearchOption option, string searchPattern)  //扩展方法的声明,需要this关键字。
        {                   
        }
     }
    

嵌套类

一般类的 访问修饰符可以定义为默认的internal 或者public,而内嵌类就有比较多的选择,可以是为protected、internal、public以及默认的private。嵌套类可以访问外部类的方法、属性、字段而不管访问修饰符的限制

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter06.Listing06_46
{
    using System;

    public class Program
    {
        // Define a nested class for processing the command line
        private class CommandLine
        {
            public CommandLine(string[] arguments)
            {
               //
            }
            public string Action;
            public string Id;
            public string FirstName;
            public string LastName;
        }

        public static void Main(string[] args)
        {
            CommandLine commandLine = new CommandLine(args);
           
        }
    }
}

分部类和分部方法

无非就是把类和方法分别放到不同的文件中去,

namespace AddisonWesley.Michaelis.EssentialCSharp
{
// We dont fully implement our switch block here
#pragma warning disable CS1522
    using System;

    // File: Program.cs
    partial class Program
    {
        static void Main(string[] args)
        {
            CommandLine commandLine = new CommandLine(args);

            switch(commandLine.Action)
            {
                // ...
            }
        }
    }

    // File: Program+CommandLine.cs
    partial class Program
    {
        // Define a nested class for processing the command line
        private class CommandLine
        {
            public CommandLine(string[] args)
            {
                //not implemented
            }

            // ...
            public int Action
            {
                get { throw new NotImplementedException(); }
                set { throw new NotImplementedException(); }
            }
        }
    }
#pragma warning restore CS1522
}

使用关键字partial ,这里就不深入探讨了。

本篇博客从学习到完成历时两天,感觉学到了特别多的干货和东西,最重要的就是从设计的角度去认知为什么要这么设计类,希望能帮助到大家。

发布了240 篇原创文章 · 获赞 114 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/sinat_33087001/article/details/103445354
今日推荐