C#高级编程第11版

【1】3.1 创建及使用类

1、构造函数:构造函数的名字与类名相同;

使用 new 表达式创建类的对象或者结构(例如int)时,会调用其构造函数。并且通常初始化新对象的数据成员。

除非类是静态的,否则会为没有构造函数的类,自动生成一个默认构造函数,并使用默认值来初始化对象字段。

构造函数可以有参数,可以以多态的形式存在多个构造函数。

构造函数分为:实例构造函数,静态构造函数,私有构造函数。

 

参考:https://www.cnblogs.com/cang12138/p/8297270.html#top

【2】3.2 类和结构

class传递的是引用地址,而struct是直接传递的值。

【3】3.3.1 字段

var customer1 = new PhoneCustomer();
customer1.FirstName = "Simon";

通过customer1.FirstName的语法访问字段。

常量可以跟变量一样与类相关联。

【4】3.3.2 只读字段

为了保证一个对象的字段不会被修改,你可以用readonly关键字声明该字段。只读字段只允许在构造函数的时候进行赋值。

public class DocumentEditor
{
    private static readonly uint s_maxDocuments;
    static DocumentEditor()
    {
        s_maxDocuments = DoSomethingToFindOutMaxNumber();
    }
}

值得一提的是,你并不是必须在构造函数里给readonly字段赋值。如果你没有主动赋值,字段将会使用其声明类型的默认值或者你在声明语句中使用的初始值。这个设定对static和实例readonly字段都有效。

最好不要将字段声明为public。

将字段声明成private并且用property来访问字段,是一个比较好的编程实践。

【5】3.3.3 属性

 1.下划线开头的变量名称很明显地看出这是个字段。

private string _firstName;

get访问器,set属性

2.属性的一些表达式:

可访问内部字段的:

private string _firstName;
    public string FirstName
    {
        get
        {
            return _firstName;
        }
        set
        {
            _firstName = value;
        }
    }

通过表达式来简写属性的get/set访问器:

private string _firstName;
public string FirstName
{
    get => _firstName;
    set => _firstName = value;
}

大括号{}和return都可以省略。

自动实现属性:

public int Age { get; set; }
public int Age { get; set; } = 42;//自动实现的属性可以像下面这样初始化

编译器会自动帮你生成一个内部字段,然后将它与属性关联,这种方式我们称之为自动实现(auto-implemented)属性。如果你只是想提供一个可读写的属性,你可以使用自动实现。使用自动生成的属性,你无法在setter中对赋值做任何有效性的判断,因为写不了任何判断语句。

【6】3.3.3.3 属性的访问修饰符

IL,JIT,MethodImpl,MethodImplOptions.AggressiveInlining

【7】3.3.3.4 只读属性

C#允许你创建只有getter而没有setter的只读属性。

private readonly string _name;
public string Name
{
    get => _name;
}

提示:C#也支持省略getter只带有setter的只写属性的方式,但这种方式不太推荐,因为在调用的时候会造成困扰。如果要支持只写字段(例如Password),建议是用方法(如SetPassword)代替只写属性。

【8】3.3.3.5 自动实现的只读属性

在声明时可初始化;只读属性也可显式地在构造函数里声明。

【9】3.3.3.6 表达式属性

只读属性直接连get都可以省略。

【10】3.3.3.7 不可变类型

一个典型的不可变类型是String类。

通过使用readonly修饰符,编译器负责解释类型的状态是否发生变化,这种类型仅允许在构造函数里进行初始化。

【11】3.3.4 匿名类型

使用var关键字搭配上new,你就创建了一个匿名类型。

匿名类型的内部成员也允许是被推断(inferred)出来的。

这么写会报错:

var captain = new
{
    person.FirstName="John" //无效的匿名类型成员声明符。匿名类型成员必须使用成员赋值、简单名称或成员访问来声明。
};

【12】3.3.5.1 声明方法

在C#里,一个方法的定义包含不少修饰符(例如方法的访问等级),接下来是返回值类型,然后是一个方法的名称,然后是一对小括号包着的一系列输入参数,然后是一组大括号,中间包含着方法体。

如果方法带有返回值,在代码结束的位置,就需要使用一个return语句,返回方法指定的值类型。

方法声明成void类型的时候,return语句不是必须的。

【13】3.3.5.2 表达式体的方法

public bool IsSquare(Rectangle rect)
{
    return (rect.Height == rect.Width);
}
public bool IsSquare(Rectangle rect) => rect.Height ==rect.Width;

=>操作符,用来区分左侧的定义和右侧的实现。

下面的例子和上面的IsSquare实质上是同一个方法,只不过我们用=>表达式的语法来重新实现它。Lambda操作符的右侧是方法的具体实现,不需要{}和return关键字,你仅仅需要保证右侧的语句返回的值类型与左侧的方法定义一致即可,在下面的代码实例里返回的是bool类型。

【14】3.3.5.3 调用方法

使用$字符可以在字符串中调用方法:

Console.WriteLine($"Pi is {Math.GetPi()}");

【15】3.3.5.4 方法重载

要么带有不同类型的参数,要么参数数量不同,是使用方法重载的必要条件。

同名方法。

【16】3.3.5.5 命名参数

所有的方法都可以使用命名参数,你只需要在传递参数值前写上参数名,然后再加上一个:,编译器会自动忽略参数名和:进行调用。

r.MoveAndResize(x: 30, y: 40,  20, height: 40);

【17】3.3.5.6 可选参数

为可选参数提供一个默认值,编译器会认为第二个参数使用它的默认值。

public void TestMethod(int notOptionalNumber, int optionalNumber = 42)
{
    Console.WriteLine(optionalNumber + notOptionalNumber);
}
TestMethod(11); //相当于TestMethod(11,42);
TestMethod(11, 22);

默认情况下会按顺序进行调用。

【18】3.3.5.7 可变数量的参数

params

int数组;object数组;

如果你的方法不止一个参数,你又想使用params定义的可变参数的话,params只能使用一次,并且它必须是最后一个参数:

Console.WriteLine(string format, params object[] arg);

【19】3.3.6 构造函数

不需要任何返回值类型。

如果你没提供构造函数,编译器会为你自动生成一个默认的无参构造函数,然后为所有的字段初始化(为引用类型赋null值,数值类型赋值为0,布尔类型赋值为false)。

你可以提供任意数量的重载构造函数,只要确保他们的函数签名不同即可:

public MyClass() // zeroparameter constructor
{
    // construction code
}
public MyClass(int number) // another overload
{
    // construction code
}

一旦你提供了带参的构造函数,编译器不会再自动给你生成一个无参构造函数。

如果你想通过无参函数来实例化一个对象的话,将会得到一个编译错误。

将构造器声明成private或者protected也是允许的,这样它们对于无关的类就是不可见的。

这种写法通常在两种情况里会很有用:

  • 如果你的类只是一个提供静态成员或者属性的容器类,那么它不必要也应该不被实例化。在这个应用场景中,其实你可以直接对class使用static声明,这种用static修饰的类智能包含static成员并且无法被实例化。
  • 如果你想让类仅仅通过调用一个static成员函数进行实例化(这种对象实例化就是所谓的工厂模式)。其中一种单例模式的实现代码如下所示:
public class Singleton
{
    private static Singleton s_instance;
    private int _state;
    private Singleton(int state)
    {
        _state = state;
    }
    public static Singleton Instance
    {
        get => s_instance ?? (s_instance = new Singleton(42));
    }
}

Singleton类只包含一个private构造函数,因此只能在它自己内部调用来进行实例化。为了提供该类的实例,提供了一个static修饰的属性Instance,它返回一个私有字段s_instance。如果s_instance未被初始化(null),一个新的实例将会通过私有的构造函数进行创建,并赋值给s_instance。这里我们用到了联结操作符??,如果操作符左侧的值为null,则会执行右侧的语句(此时调用了Singleton的构造函数),并最终返回操作符右侧的值。

联结操作符??

【20】3.3.6.1 用表达式书写构造函数

如果构造函数的实现代码仅仅只包含一行表达式,那么它就可以用表达式的方式简写:

public class Singleton
{
    private static Singleton s_instance;
    private int _state;
    private Singleton(int state) => _state = state;//
    public static Singleton Instance => s_instance ?? (s_instance = new Singleton(42));
}

【21】3.3.6.2 调用其它构造函数

当有多个构造函数,实现初始化。

 this关键字简单的调用了最近的参数匹配的(nearest matching)构造函数。

【22】3.3.6.3 静态构造函数

C#也允许使用static关键字来修饰一个无参构造函数。这个构造函数只会被执行一次,不像其他类型的构造函数,它只会在第一个实例创建的时候,被执行一次。

静态构造函数没有任何访问修饰符。无法在C#代码里显式调用它,通常当.NET运行时加载该类时就自动调用了。

【23】3.4 结构

既没有需要实现各种方法,也没有必要继承其他类,仅仅只是存储两个double类型的变量。

class改成struct即可。

struct是值类型,而非引用类型。这意味着,它们要不单独存储在栈Stack里,要不就在托管堆Heap里的某一行(当它们作为某个实例对象的成员时),并且跟简单数据类型(如int,bool等)拥有相同的生命周期限制:

  • 结构体不支持继承。
  • 如果你没有提供任何构造函数,编译器同样会为结构体创建一个无参构造函数,并初始化各个成员。就算你提供了带参构造函数,你也可以通过无参构造函数来new一个结构体,而class不行。
  • 使用结构体,你可以指定字段在内存中如何存储(lay out)。

声明成public。

【24】3.4.1 结构是值类型

class与struct

class:

var point = new Dimensions();
point.Length = 3;
point.Width = 6;

struct:

Dimensions point = new Dimensions();
point.Length = 3;
point.Width = 6;

或:

Dimensions point; //直接省略new
point.Length = 3;
point.Width = 6;

或:

Dimensions point = new Dimensions(3, 6);

struct中不能实例属性或字段初始值设定项,我们无法像类那样提前初始化Length和Width。不能这样:

Dimensions point; //struct中不能实例属性或字段初始值设定项,我们无法像类那样提前初始化Length和Width
double d = point.Length; //使用了未赋值的局部变量point

struct和其他数据类型遵循同样的规则:变量使用前必须先初始化。

结构体是用来存储那些足够小的关联数据结构的。

【25】3.4.2 只读结构

struct定义的值类型是不可变类型,无论怎么操作,原struct的值是不会变化的。

添加一个readonly的修饰符,编译器可以确保struct的不可变性。

【26】3.4.3 结构和继承

所有的struct都直接派生自ValueType,ValueType又派生于System.Object。

ValueType类相对于其父类Object来说并没有新增任何成员,只是提供了一些Object方法的override实现,以便更适合struct使用。注意你无法为struct指定另外的基类。

注意:

  • 只有当structs当objects一样使用的时候,才会用到ValueType的继承性。而ref structs无法当成objects进行使用。本章3.4.5会更详细的解释ref structs,这里的ref structs说的不是在方法里用ref修饰的形参,而是另外一种结构体。
  • 为了比较两个结构体值,最好的方式是实现IEquatable<T>接口

【27】3.4.4 结构的构造函数

可以像定义类的构造函数那样定义结构体的构造函数。

结构体总是隐式提供了用来给所有字段赋初值的无参构造函数。

【28】3.4.5 ref 结构

struct并非总是存储在栈里的。它们也可能存在于堆中。

需要保证指针类型永远是存储在栈上的。

只允许存在于栈上的值类型,这种类型就是用ref关键字修饰的结构体:

ref struct ValueTypeOnly
{
    //...
}

【29】3.5 按值和按引用传递参数

为了避免struct和class之间的误用,其实最好的方式是保证struct是不可外部赋值(immutable)的。

【30】3.5.1 ref 参数

给方法传递的参数都必须先进行初始化,不管它是通过值传递还是引用传递的。

了让调用方能明确知道自己是传递的参数地址,在调用方法的时候,你需要显式地写上ref修饰符。

为参数加上ref修饰符,这样即便A是struct类型,它也会传递引用地址。

【31】3.5.2 out 参数

如果一个方法需要一个返回值,这个方法往往定义成该返回值的类型,并且返回相应类型的结果。那么要返回多个值的时候怎么办?

一个方式是定义一个类或者结构体,并且将所需的返回值都定义成它们的成员,最终返回这个类和结构体。另外一种方式是可以使用元组类型,这个我们在第13章的时候再详细讲述。而第三种方式,则是使用out关键字。

调用这个方法,out修饰的result变量不需要提前初始化,这个变量由方法内部赋值。

方法调用的时候,传递参数前需要显式使用out关键字。

【32】3.5.3 in 参数

in操作符则保证传递给方法的数据不会发生任何改变(当传递的是一个值类型时)。

如何在方法中再给这个赋值,那么会弹出错误。

也可以将它用在引用类型上。在这种情况下,你只能修改引用类型内部成员,而不能修改引用类型自身。

假定我们将上面的AValueType修改为class,那么:

struct AValueType
{
    public int Data;
}


private static void CantChange(in AValueType a)
{
    a = new AValueType(); // 错误: 无法分配到变量'in AValueType',因为它是只读变量
    a.Data = 43;  // 允许修改的,没有问题
    Console.WriteLine(a.Data);
}

【33】3.6 可空类型

可空类型(nullable types)。可空类型是值类型,但又允许设置成null值。

你只需要在类型(必须是struct)后面加上?标识即可。

int x1 = 1;
int? x2 = null;

因为一个普通的int没有任何不能赋给int?的值,因此用int变量直接给int?类型的变量赋值对于编译器来说是允许的:

int? x3 = x1;

int?不能直接赋值给int,除非你显式的指定一个强制转换:

int x4 = (int)x3;

可空类型的两个属性HasValue和Value。HasValue顾名思义返回一个true或者false,取决于可空类型是否为null,而Value属性则返回实际值。通过使用条件运算符?。

int x5 = x3.HasValue ? x3.Value : -1;

通过联结运算符??更是可以将赋值简写成下面这样:

int x6 = x3 ?? -1;

【34】3.7 枚举类型

1.enum类型的内在类型是int。内在类型可以转换成其他任意的整型类型(如byte,short,int,long,无符号数或者有符号数)。

2.DaysOfWeek

3.Enum的TryParse方法来将一个string类型的字符串转换成相应的Color枚举

4.Enum.TryParse<T>()是一个泛型方法,其中T是泛型参数类型。

5.Enum.GetNames方法则会返回一个带有所有常量名称的string[]数组

【35】3.8 部分类

1.partial关键字让你能够讲class,struct,method或者interface分别存到不同文件中。

2.类似:

[AnotherAttribute]
partial class SampleClass: IOtherSampleClass
{
    public void MethodTwo() { }
}

【36】3.9 扩展方法

1.扩展方法(Extension Method)则是另外一种为某个类追加新功能的方案(这种方法甚至可以做到继承无法达成的部分,譬如当一个类被声明成sealed时,它就无法被继承)。

注意:扩展方法甚至可以用来扩展接口,通过这种方式你可以为所有实现了该接口的类统一追加新功能。

2.扩展方法是static方法,看起来像是所属类中的一部分,事实上,编译后它并不存在这个类中

3.LINQ使用了很多扩展方法

【37】3.10 Object 类

1.不仅方法,属性又或者其他一切你定义的内容,都可以访问到Object类里定义的public或者protected成员。这些方法同样适用于那些不是你定义的类。

2.

下面总结了Object类中的方法和它们的用途:

  • ToString:一个相当基础,快捷,简单的字符串显示方法。使用这个方法,你可以快速的了解某个对象的内容,譬如在调试的时候。关于如何格式化数据它几乎没有提供太多的选择。举个例子,日期类型的数据可能会有各种各样的格式,但是DateTime.ToString方法在这方面没有给你提供任何选择。如果你需要一个更丰富的字符串展示——例如,当你考虑根据指定的格式或者不同的文化(或者区域)显示不同风格的日期的时候——你可能需要实现IFormattable接口(第九章将会涉及这部分内容)。
  • GetHashCode:如果对象存储在某些特定的数据结构,譬如图,又或者哈希表和字典中时,它需要通过创建这些对象的类来决定在数据结构中的何处存储这些对象。假如你想让你的类作为字典中的键值,你需要重写该类的GetHashCode方法。我们将在第十章的时候介绍这部分,因为如何实现这个方法的重载有一些相当严格的要求。
  • Equals(全版本)和ReferenceEquals:你可能已经注意到,在.NET中存在三种不同的方法用来比较两个对象是否相等,包括操作符==在内,他们仨有一些细微的区别。此外,如何重写带有一个参数版本的Equals虚方法还存在一定的限制,因为System.Collections命名空间里的基类会调用这个方法,因此它需要能得出一个确定的结果。第六章的时候你将会探索这些方法的使用。
  • Finalize:第十七章将会包括这部分内容,这个C#方法非常像C++风格的析构函数(destructors)。当GC垃圾回收需要清理无用的引用对象时被调用。Object类定义了Finalize方法但是里面没有任何实现,所以GC会忽略这一部分。通常你可以在一个对象引用非托管资源的时候,重写此方法,因为默认GC只会处理托管资源,而非托管资源如何释放取决于你提供的Finalize方法。
  • GetType:这个方法返回一个派生自System.Type的类实例,因此它可以提供更加广泛具体的信息,你调用GetType方法的对象将会成为这个实例中的一个成员,方法还会提供给你其它信息,譬如基类,方法名,属性等等。System.Type还是.NET反射技术的入口。第十六章将会详细介绍这部分。
  • MemberwiseClone:唯一一个本书没有详细介绍的方法。这是因为这个方法从概念上看就相当简单。它只是单纯地拷贝一个对象,并返回拷贝后的引用(对于值类型,则装箱后返回装箱对象的引用)。注意这个拷贝只是浅拷贝,这意味着它只拷贝了目标class中所有的值类型,假如目标class中还含有任何引用,那么它只拷贝引用,而不拷贝引用指向的实际对象。这个方法是protected修饰的,因此并不能用来拷贝外部对象。并且它也不是virtual方法,所以你也无法重写它。

【38】扩展

参考网址:https://www.cnblogs.com/zenronphy/p/ProfessionalCSharp7Chapter3.html#32-%E7%B1%BB%E5%92%8C%E7%BB%93%E6%9E%84

原文地址:https://www.cnblogs.com/wazz/p/14081519.html