《c#10 in a nutshell》— 读书随记(3)


Chaptor 3 . Creating Types in C#

内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的

Classes

类的格式是:

  • 在前面的是Attributes and class modifiers:public, internal,abstract, sealed, static, unsafe, and partial.

  • 然后是类的名字

  • 最后的是大括号包裹的类的成员:methods, properties, indexers, events, fields, constructors,overloaded operators, nested types, and a finalizer

Fields

字段是类的在其中一个成员,其实就是一个变量。它支持以下几种修饰符

《c#10 in a nutshell》--- 读书随记(3)

The readonly modifier

readonly这个修饰符是为了防止类实例化之后,字段被修改,它只能在构造函数中或者在字段中直接初始化

Declaring multiple fields together

可以同时声明并初始化多个字段

static readonly int legs = 8,
                    eyes = 2;

Methods

一个方法的签名必须是独一无二的(在当前类),函数的签名包括它的名字和参数列表,但不包括函数的返回值,函数允许的修饰符有:

《c#10 in a nutshell》--- 读书随记(3)

Expression-bodied methods

函数体有两种写法:

第一种是普遍的写法,也就是函数签名后跟大括号

还有一种就是函数签名后跟=>箭头,然后是表达式,而且只能是单行,这叫做expression-bodied method

Static Local methods

可以在方法的内部,再声明一个本地方法,如果是正常的本地方法,它就好像是闭包一样,可以捕捉周围的本地变量,如果是在本地方法加上静态修饰符,那么它就没办法捕捉本地变量了,这可以防止某些本地方法错误捕捉周围的变量

Instance Constructors

Constructor and field initialization order

字段的初始化是发生在构造函数之前的,字段初始化的顺序是编写代码的顺序

Deconstructors

解构函数,相当于是构造函数的反函数,一个构造函数代表获取一个集合的数据并将它们赋值给字段,然后一个解构函数就代表将字段中的值赋值到一个集合的变量中

解构函数必须的名字必须是Deconstruct,然后必须有一个或多个out参数

public void Deconstruct (out float width, out float height)
{
    width = Width;
    height = Height;
}

然后在使用的时候,不需要特殊的标记,只需要赋值

var rect = new Rectangle (3, 4);
(float width, float height) = rect;    // Deconstruction
var (width, height) = rect;    // simply

需要注意的是,解构函数也可以编写为扩展函数,也就是说,如果需要对某一个类型解构,但是类型的作者没有提供,那我们可以自己编写一个扩展函数解构类型

Object Initializers

对象初始化是发生在构造函数执行后,可以给任何公开的字段或属性进行赋值。

Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false };
Bunny b2 = new Bunny ("Bo")
{ LikesCarrots=true, LikesHumans=false };

// 编译器会生成下面类似的代码

Bunny temp1 = new Bunny();
temp1.Name = "Bo";
temp1.LikesCarrots = true;
temp1.LikesHumans = false;
Bunny b1 = temp1;

Properties

属性看起来像是字段。但是实际上,属性包含逻辑,像是方法一样。比如下面的代码

Stock msft = new Stock();
msft.CurrentPrice = 30;
msft.CurrentPrice -= 3;
Console.WriteLine (msft.CurrentPrice);

单纯从外面看,你不知道这是属性还是字段

属性的声明像一个字段,但是不同的是,属性有两个访问器get和set

public class Stock
{
    decimal currentPrice;

    public decimal CurrentPrice

    {
        get { return currentPrice; }
        set { currentPrice = value; }
    }
}

get访问器方法,必须返回和属性类型一样的值,而set方法接受一个和属性类型一样的参数value。其实相当于两个方法,这两个方法并不一定和上面的代码一样,要有一个相同名字的字段存储值,它同样可以返回计算值。

属性的可用修饰符

《c#10 in a nutshell》--- 读书随记(3)

Read-only and calculated properties

如果一个属性只有get访问器,那么这个属性就是只读属性

Expression-bodied properties

public decimal Worth
{
    get => currentPrice * sharesOwned;
    set => sharesOwned = value / currentPrice;
}

Automatic properties

最通用的属性实现是简单读取和写入一个私有的字段。那么可以用automatic property声明来让编译器帮我们实现

public decimal CurrentPrice { get; set; }

Property initializers

可以在Automatic properties的基础上,给属性初始化一个值,只需要

public decimal CurrentPrice { get; set; } = 123;

Init-only setters

public class Note
{
    public int Pitch { get; init; } = 20;
    public int Duration { get; init; } = 100;
}

init-only的行为就像是一个read-only的属性

Indexers

索引器提供了一种自然的语法来访问类或者结构体的元素。它和属性很相似,但是它是通过索引参数来访问而不是属性名。比如说string就有一个索引器,它就像数组一样访问元素,不同的是它的索引参数可以是任意类型

string s = "hello";
Console.WriteLine (s[0]); // 'h'
Console.WriteLine (s[3]); // 'l'

Implementing an indexer

class Sentence
{
    string[] words = "The quick brown fox".Split();

    public string this [int wordNum]
    {
        get { return words [wordNum]; }
        set { words [wordNum] = value; }
    }
}

而且索引参数还可以是多个

public string this [int arg1, string arg2]
{
    get { ... } set { ... }
}

Using indices and ranges with indexers

只需要将索引器的参数标记为Index或者Range就可以了

Static Constructors

静态的构造函数,每种类型只会执行一次,而不是每次实例化都会执行。而且必须是无参构造

Static constructors and field initialization order

静态字段的初始化发生在静态构造函数的之前。如果类没有静态构造函数,那么静态字段的初始化发生在这个类型被使用的时候

Console.WriteLine (Foo.X);    // 3

class Foo
{
    public static Foo Instance = new Foo();
    public static int X = 3;
    Foo() => Console.WriteLine (X);    // 0
}

Static Classes

静态类必须由静态成员组成,类不能被实例化或子类化

Finalizers

这个方法是唯一一个,在实例被垃圾回收器回收时所调用的方法

class Class1
{
    ~Class1()
    {
        ...
    }
}

The nameof operator

nameof()操作符,会返回任何符号的名字的字符串

Inheritance

C#只能单继承,用来表示继承或者实现

public class Asset
{
    public string Name;
}

public class Stock : Asset
{
    public long SharesOwned;
}

Polymorphism

引用是多态的。这说明,一个变量x,可以引用一个对象y,y属于x的子类

Casting and Reference Conversions

一个引用对象可以:

  • 隐式地向上转换到基类的引用
  • 显式地向下转换到子类的引用

The as operator

在之前,如果向下转换时,失败了,会抛出转换错误的异常,但是现在可以使用关键字as来转换,如果转换失败了,引用只会指向null,而不是抛出异常

Asset a = new Asset();
Stock s = a as Stock;   // s is null; no exception thrown

The is operator

is操作符用来测试一个变量是否匹配某个模式。C#有几种模式,最重要的一种是type pattern类型模式。在这个场景下,is操作符会测试一个引用的转换是不是成功

if (a is Stock)
    Console.WriteLine (((Stock)a).SharesOwned);

Introducing a pattern variable

可以直接转换成某个类型的变量

if (a is Stock s)
    Console.WriteLine (s.SharesOwned);

Virtual Function Members

标记为virtual的函数,可以被子类重写。方法、属性、索引器和事件都可以声明为virtual

public class Asset
{
    public string Name;
    public virtual decimal Liability => 0;
}

public class House : Asset
{
    public decimal Mortgage;
    public override decimal Liability => Mortgage;
}

Covariant return types

返回值是返回协变体是允许的

public class Asset
{
    public string Name;
    public virtual Asset Clone() => new Asset { Name = Name };
}

public class House : Asset
{
    public decimal Mortgage;
    public override House Clone() => new House { Name = Name, Mortgage = Mortgage };
}

Hiding Inherited Members

子类的成员和基类的成员一样的时候,子类的成员会覆盖基类的成员

public class A{ public int Counter = 1; }
public class B : A{ public int Counter = 2; }

但是编译器会有warning,所以最好的做法应该是用关键字new

public class B : A { public new int Counter = 2; }

那么new重写和overrider重写的区别是,new是覆盖,当基类引用指向子类实例的时候,调用方法时会使用基类的方法,但是如果是overrider重写了,那么就算是基类引用,调用的还是子类实例的方法

Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); // Overrider.Foo
b1.Foo();   // Overrider.Foo

Hider h = new Hider();
BaseClass b2 = h;
h.Foo();    // Hider.Foo
b2.Foo();   // BaseClass.Foo

Sealing Functions and Classes

一个overrider的函数,可以用sealed关键字阻止它的子类继续重写这个实现

这个关键字还可以用在类上,可以阻止派生子类

需要注意的是,new覆盖是不可以用sealed

Constructors and Inheritance

Constructor and field initialization order

  1. 从子类到基类
    • 字段初始化
    • 调用基类构造函数
  2. 从基类到子类
    • 构造函数体执行
public class B
{
    int x = 1;      // 3
    public B (int x)
    {
        ...         // 4
    }
}

public class D : B
{
    int y = 1;      // 1
    public D (int x) 
    : base (x + 1)  // 2
    {
        ...         // 5
    }
}

Boxing and Unboxing

装箱是一个值类型转换到一个引用类型,这个引用类型可以是一个object类型或者一个接口

int x = 9;
object obj = x;     // Box the int

拆箱就是强转成值类型int y = (int)obj; // Unbox the int

The GetType Method and typeof Operator

C#可以在运行时获取实例的类型System.Type,有两种方式获取:

  • 从实例调用方法GetType
  • 对类型名称使用typeof操作符

Object Member Listing

public class Object
{
    public Object();
    public extern Type GetType();
    public virtual bool Equals (object obj);
    public static bool Equals (object objA, object objB);
    public static bool ReferenceEquals (object objA, object objB);
    public virtual int GetHashCode();
    public virtual string ToString();
    protected virtual void Finalize();
    protected extern object MemberwiseClone();
}

Structs

结构体和类很相似,不同点在于

  • struct是一个值类型,类是一个引用类型
  • struct不支持继承
  • 因为struct是值类型,所以是没有null的,它的默认值是空的结构体

Read-Only Structs and Functions

readonly struct Point
{
    public int X, Y;
}

可以在结构体上标注只读,这样结构体所有字段都是只读的,也可以给方法标注只读,这样方法如果想去修改任何字段,都会报编译时错误

Ref Structs

不像那些引用类型,都是存活在堆中的,结构体的内存是根据变量声明的位置决定的。如果一个值类型出现在参数或者本地变量中,那么它是存活在栈中的

但是如果值类型是出现在类的字段中,那它也是在堆中存活的

如果给结构体添加ref关键字,那么这个结构体只能存活在栈中

Access Modifiers

  • public,公开的,所有assembly都可以访问,它是enum或者interface的默认访问
  • internal,只能是同一个assembly的类型才能访问,这是默认的非嵌套类型访问
  • private,私有的,除了类型自己,其他都不能访问
  • protected,只能类型或者类型的子类可以访问
  • protected internal,是internalprotected的结合
  • private protected,只能同一个assembly的子类才能访问

Friend Assemblies

可以暴露internal的成员给friendassembly访问,通过使用attribute来实现
System.Runtime.CompilerServices.InternalsVisibleTo

[assembly: InternalsVisibleTo ("Friend")]

Interfaces

接口就像是无状态的类

  • 接口只能定义方法,没有字段
  • 接口的成员默认是abstract
  • 类或者结构体可以实现多个接口

Extending an Interface

接口可以继承其他接口

Explicit Interface Implementation

当两个接口具有相同的方法签名的时候,我们可以显式实现某一个接口的方法

interface I1 { void Foo(); }
interface I2 { int Foo(); }

public class Widget : I1, I2
{
    public void Foo()
    {
        Console.WriteLine ("Widget's implementation of I1.Foo");
    }

    int I2.Foo()
    {
        Console.WriteLine ("Widget's implementation of I2.Foo");
        return 42;
    }
}

如果是这样,那么这个类中实现了两个接口的方法,如果需要调用某个接口的方法,只能通过cast来做到

Widget w = new Widget();
w.Foo();            // Widget's implementation of I1.Foo
((I1)w).Foo();      // Widget's implementation of I1.Foo
((I2)w).Foo();      // Widget's implementation of I2.Foo

Implementing Interface Members Virtually

一个接口的成员实现,默认是seal密封的,如果需要作为基类需要给子类实现,需要添加关键字virtual

public interface IUndoable { void Undo(); }

public class TextBox : IUndoable
{
    public virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}

public class RichTextBox : TextBox
{
    public override void Undo() => Console.WriteLine ("RichTextBox.Undo");
}

Reimplementing an Interface in a Subclass

public interface IUndoable { void Undo(); }

public class TextBox : IUndoable
{
    void IUndoable.Undo() => Console.WriteLine ("TextBox.Undo");
}

public class RichTextBox : TextBox, IUndoable
{
    public void Undo() => Console.WriteLine ("RichTextBox.Undo");
}

一个子类可以重新实现任何基类已经实现的接口方法。上面的例子是,基类实现的方法不是virtual的,所以子类不能覆盖,子类必须实现接口并重新实现

Default Interface Members

C# 8增加了,默认方法作为接口的成员

Enums

枚举是一个特殊的类型,它是一组被命名的数字常量。

public enum BorderSide { Left, Right, Top, Bottom }

默认情况下,所有成员都是int类型,从0开始分配

也可以选择一个指定的数字类型

public enum BorderSide : byte { Left, Right, Top, Bottom }

也可以显式指定一个值给每个成员

public enum BorderSide : byte { Left=1, Right=2, Top=10, Bottom=11 }

Enum Conversions

枚举可以转换成一个数字类型,反向操作也是可以的。

int i = (int) BorderSide.Left;
BorderSide side = (BorderSide) i;
bool leftOrRight = (int) side <= 2;

Flags Enums

也可以将枚举的成员结合在一起,防止歧义,成员的结合需要显式赋值

[Flags]
enum BorderSides { None=0, Left=1, Right=2, Top=4, Bottom=8 }

可以对这个枚举使用位操作符,比如|&

BorderSides leftRight = BorderSides.Left | BorderSides.Right;

if ((leftRight & BorderSides.Left) != 0)
    Console.WriteLine ("Includes Left");    // Includes Left

string formatted = leftRight.ToString();// "Left, Right"

BorderSides s = BorderSides.Left;
s |= BorderSides.Right;
Console.WriteLine (s == leftRight);// True

s ^= BorderSides.Right;     // Toggles BorderSides.Right
Console.WriteLine (s);      // Left

而且这种枚举可以递归定义

[Flags]
enum BorderSides
{
    None=0,
    Left=1, Right=1<<1, Top=1<<2, Bottom=1<<3,
    LeftRight = Left | Right,
    TopBottom = Top | Bottom,
    All       = LeftRight | TopBottom
}

Type-Safety Issues

枚举的真实值是数字,那么在转换的时候,可以用一个没有定义的值转换,不会发生错误,可以使用方法
Enum.IsDefined进行验证

BorderSide side = (BorderSide) 12345;
Console.WriteLine (Enum.IsDefined (typeof (BorderSide), side));     // False

Generics

C#有两种机制,来使得不同的类型重用代码:继承和范型

继承表达的是重用一个基础类型,范型表达的是重用一个模板包含着一个占位符类型。范型和继承比较,它增加了类型安全和减少强转和装箱

Generic Types

范型类型声明一个类型参数,它是一个占位符,会在使用的地方填上具体的类型。

public class Stack<T>
{
    int position;
    T[] data = new T[100];
    public void Push (T obj)=> data[position++] = obj;
    public T Pop()=> data[--position];
}

var stack = new Stack<int>();
stack.Push (5);
stack.Push (10);

Stack<int>有如下定义,类名称会是一个hashed值避免混乱

public class ###
{
    int position;
    int[] data = new int[100];
    public void Push (int obj)=> data[position++] = obj;
    public int Pop()=> data[--position];
}

技术上,将Stack<T>叫做开放类型,Stack<int>叫做封闭类型。在运行时,所有的范型类型实例都是封闭的

typeof and Unbound Generic Types

运行时是不存在开放范型类型的。它们在编译时就变成封闭类型了。然而,还有一种可能是,运行时存在不受约束的范型类型,纯粹作为Type对象,唯一的方法是使用typeof操作符

class A<T> {}
class A<T1,T2> {}

Type a1 = typeof (A<>);     // Unbound type
Type a2 = typeof (A<,>);    // Use commas to indicate multiple type args.

Type a3 = typeof (A<int,int>); 
class B<T> { void X() { Type t = typeof (T); } }

The default Generic Value

可以用default关键字区获取范型类型参数的默认值,如果是引用类型的默认值是null,如果是值类型的默认值就是按位置零

Generic Constraints

范型约束,约束可以应用在类型参数中,具有更多指定的类型参数

where T : base-class    // 基类约束
where T : interface     // 接口约束
where T : class         // 引用类型约束
where T : class?        // 可空引用类型约束
where T : struct        // 值类型约束
where T : new()         // 无参构造约束
where U : T             // 裸类型约束
where T : notnull       // 不可空值类型或者不可空引用类型

Covariance

假如A可以转换成B,那么X有一个协变类型参数,比如X<A>可以转换为X<B>

需要注意的是,可以转换的意思是,A是B的子类或者实现,然后A可以隐式转换成B。数字转换、装箱转换和自定义转换都不包括在内

协变不是自动的

class Animal {}
class Bear : Animal {}
class Camel : Animal {}

Stack<Bear> bears = new Stack<Bear>();
Stack<Animal> animals = bears;          // Compile-time error

class ZooCleaner
{
    public static void Wash<T> (Stack<T> animals) where T : Animal { ... }
}

Stack<Bear> bears = new Stack<Bear>();
ZooCleaner.Wash (bears);

Declaring a covariant type parameter

可以在接口或者委托上声明一个协变类型参数,通过将它标记为out

public interface IPoppable<out T> { T Pop(); }

这个out修饰符标记的意思是T只是被用来作为输出的参数

var bears = new Stack<Bear>();
bears.Push (new Bear());    // Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>:

IPoppable<Animal> animals = bears;  // Legal
Animal a = animals.Pop();

可以这样转换,是因为在限制的前提下,这种转换是类型安全的,因为编译器是阻止调用将T作为输入参数的方法,out修饰符保证了这个类型参数只会出现在输出参数中,而不会输入

Contravariance

逆变,是协变的逆转。比如说A可以隐式转换为B,然后X<A>可以协变为X<B>,如果是逆变,那就是X<B>转换为X<A>。这个可以用在,当类型参数T只会出现在输入参数中,可以用in修饰符,这样我们就可以做到

IPushable<Animal> animals = new Stack<Animal>();
IPushable<Bear> bears = animals;    // Legal
bears.Push (new Bear());

C# Generics Versus C++ Templates

C#的范型和C++的模板非常相似,但是内部运行的机制是非常不一样的。两个都是必须是声明的和实际使用的结合在一起,声明的占位符必须让使用者填上。然而,C#的范型是可以编译成库的,因为声明和使用的结合成封闭类型是一直到运行时才会发生。但是C++模板的话,是发生在编译时的。这意味着C++不能发布模板库,它们只会存在源码中。

原创文章,作者:254126420,如若转载,请注明出处:https://blog.ytso.com/271126.html

(0)
上一篇 2022年7月3日
下一篇 2022年7月3日

相关推荐

发表回复

登录后才能评论