从这一章开始,基本就脱离了结构化编程的思维,所以我的博客排版方式也会摒弃之前的节选方式,而是深入的去理解和描述内容,而这本书给我带来的惊喜就是:能够从设计的角度去帮助我理解为何类要如此设计,其设计缘由是什么,我觉得这一点远比单纯的告诉你怎么用更重要!感觉这一章的干货特别多,可以让我的面向对象思维上一个小小的台阶,这里将自己的学习笔记以及自己的理解整理分享出来,希望对大家都有所帮助,还有一点要说明下:面向对象思维远比语言的选择更重要,所以无论Java、C++还是C#,不同的语法只是为了满足自己特定场合的用途,而核心思想才是最重要的,实现方式反而没那么需要重视(R语言也一样哦,你想学,我就提前学学教你hhh)!
有别于作者的排列方式,我想先宏观的串讲下概念,然后再填充细节。采用连续发问的方式来引发你的思考和明白没一项的场景需求:
1,为什么要有类?
相较于之前5章对面向过程编程层面的理解,我觉得面向对象编程的好处就是:可以不必从头创建新程序,而是用现有的一个模板去复制、去扩展,或添加更多,而且整个代码还非常有条理,可以控制访问权限、组织起来不会乱,还有就是理解起来也更加容易,而类就是用来完成面向对象的使命的。
2,面向对象的三大特性:继承、封装、多态?
依据面向对象编程的好处,我们可以确定,面向对象比面向过程的优势:可扩展、好组织、能控制访问权限。所以对应的类一定要具有这样的能力:封装类的细节、派生类型来扩展基类型的能力(包括数据和方法)、方法或类型的多态化实现。
3,如何实现封装这一特性(本篇只介绍封装部分,下一章介绍继承和多态)?
这就需要很好的组织类的成员,先来了解一下:实例字段、静态字段、实例方法、静态方法、普通构造函数(实例)、静态构造函数、嵌套类。用访问修饰符来限定以上内容的访问方式,当然,如果限定了private的字段,我【实例.字段】是看不到私有成员的,但是我又想给该字段赋值,或者想让数据在外部只读,而能内部修改,简而言之就是想让字段能具有更加细粒度的操作内容,怎么办呢,传统的方式是使用public的get和set方法,但是C#提供了属性来简化这一实现。
4,既然我都有了实例字段、实例方法,我为啥还需要静态的字段和方法?书上有个例子举的特别好:
类和对象都能关联数据。将类想象成模具,将对象想象成根据该模具浇铸的零件,可以更好地理解这一点。例如,一个模具拥有的数据可能包括:到目前为止已用模具浇铸的零件数、下个零件的序列号、当前注入模具的液态塑料的颜色以及模具每小时生产零件数量。类似地,零件也拥有它自己的数据:序列号、颜色以及生产日期/时间。虽然零件颜色就是生产零件时在模具中注入的塑料的颜色,但它显然不包含模具中当前注入的塑料颜色数据,也不包含要生产的下个零件的序列号数据。
设计对象时,程序员要考虑字段和方法应声明为静态还是基于实例。一般应将不需要访问任何实例数据的方法声明为静态方法,将需要访问实例数据的方法(实例不作为参数传递)声明为实例方法。静态字段主要存储对应于类的数据,比如新实例的默认值或者已创建实例个数。而实例字段主要存储和对象关联的数据。
5,扩展方法有啥意义?
其实我感觉没啥意义,就是如果一个已经写好的类缺少你比较需要的一个功能,可以通过扩展方法来创造一个扩展类出来,然后在里面写一个扩展方法,这样你的类的对象就可以直接调用该扩展方法了,就好像你的类本身就具有这样的方法一样。其实我感觉这种方式用的应该比较少,如果对象想实现一个类没有的方法,完全可以搞个派生类出来。
6,嵌套类有啥用?
如果一个类在它的包容类外部没有多大意义,就适合设计成嵌套类,换言之,这个类对于它的包容类比较专属,通过嵌套的方式可以限定它不被滥用。
7,为啥要有分部类和分部方法
我这么理解:其实说白了就是,一个类或方法如果在修改的时候有一部分老是不变,就把它单独抽成一个文件呗,避免反复无意义的重新生成,而且有时候如果多个程序员同时改一个类,虽然有git版本控制,但是拆开来岂不是更好。
类的声明和实例化其实已经不必赘述了,需要注意的就是以下两点规范:
而对象就是类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支持直接赋值
属性有如下几条使用规范,最需要注意的就是,属性代表数据,而方法代表行动,不用混为一谈。
当然也有一定的书写和命名规范:
总之优先使用自动实现的属性,使用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代码,属性就是一种方法实现,我可以理解其为一种语法糖:
其实构造函数都比较熟悉了,就聊一点儿需要注意的地方:
构造函数可以重载,一个使用规范是:要优先调用可选参数的方法而不是重载。
对象初始化器,其实就是一种语法糖,需要注意的是,初始化时会按顺序赋值:
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就是常量的意思,默认为静态字段,而且只限于字面值类型(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 ,这里就不深入探讨了。
本篇博客从学习到完成历时两天,感觉学到了特别多的干货和东西,最重要的就是从设计的角度去认知为什么要这么设计类,希望能帮助到大家。