ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2022-07-02 22:00:28  阅读:184  来源: 互联网

标签:10 c# public int 类型 new Foo class 随记


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

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

The readonly modifier

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

Declaring multiple fields together

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

static readonly int legs = 8,
                    eyes = 2;

Methods

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

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。其实相当于两个方法,这两个方法并不一定和上面的代码一样,要有一个相同名字的字段存储值,它同样可以返回计算值。

属性的可用修饰符

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++不能发布模板库,它们只会存在源码中。

标签:10,c#,public,int,类型,new,Foo,class,随记
来源: https://www.cnblogs.com/huangwenhao1024/p/16438679.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有