C# - 引用类型


引用类型(Reference Type)

C#是一门使用OOP技术的编程语言(Object Oriented Programming 面向对象编程)面向对象最重要的特性就是接口、继承、多态 C#中所有的事物都可以看做是一个对象,对象由类型来创造,而类型就相当于一个蛋糕模型,将面粉填满这个模型且送进烤箱,最后烘焙出来的就是对象。烘焙的过程即对象诞生的过程,在面向对象编程的世界里这个过程被称为实例化对象,一旦创建完成,则该对象就有了该类型的属性、字段、方法等一切可以被对象访问的事物。类型本质上则是一个组合了方法及其相关数据的一个包容体。当我们存放的数据不能用一个简单的基元类型来存储的时候,就可以使用这种复杂的类型了。

类(Class)

类型修饰符

类有两种类型的修饰符,权限修饰符和形态修饰符,权限是必须的修饰符,而形态总是可选的项。 

//定义类的格式: 权限修饰符 形态修饰符 class关键 类名称
//因为类的默认权限是internal 所以权限修饰符就变成可选的 
//形态修饰符本身就是可选的 所以最终格式可以是这样:class关键字 类名称

1.权限修饰

用于定义类型的访问权限。

Ⅰ.internal(内部) 默认

该类只能在定义它的命名空间中被访问。 如果没有特殊需求,一般都将类设为public,因为internal的类不能在它所属于的命名空间之外被访问,也就失去了类作为封装数据并提供复用操作的意义,所以为什么不将public设为默认项而将internal设为默认项,有点搞不明白。

Ⅱ.public(公有) 

该类可以在任何命名空间中被访问。  

2.形态修饰

用于定义类型的形态。

Ⅰ.abstract(抽象)

该类不能new,可继承。可以这样理解:抽象的东西是无形之物,因为其无形,所以无法new出具体的样貌,但具体的事物可以从其派生,因为具体的事物全部源自无形的虚空。

Ⅱ.sealed(密封)

该类不能继承,可new。可以这样理解:封闭之物是尊崇自我的有形之物,因为其有形,所以可new出具体的样貌,但不能从其派生。因为它完全彻底的封闭自我,它不结婚生子,所以不可能从其派生。

Ⅲ.static(静态)

在内存中是唯一的,综合了抽象和密封的特点,也即它不能new、不能从其派生。

定义类

定义类时, 先声明权限,再声明形态(可选), 如:

public class Myclass
{
    //成员略……
}

定义抽象类

抽象类提供能体现某些基本行为的成员,但只做声明而无具体的实现逻辑。抽象类是不能被实例化的。派生自抽象类型的子类型必须override基类的所有抽象成员。如果不实现,则子类型也必须声明为是抽象类型。另外,抽象类既可以定义抽象成员同时也可以定义非抽象的成员。但字段不能声明为抽象字段,成员只有抽象属性和抽象方法。 

public abstract class 虚无
{
    public abstract string 幻象 { get; set; } //抽象属性
    public abstract void 涅盘(); //没有方法体,这是一个抽象方法
}

public class 存在 : 虚无
{
    public override string 幻象 { get { return "无垠"; } set { 幻象 = value; } }
    public override void 涅盘()
    {

    }
}

派生类重写抽象基类的成员,那么这些重写的成员实现就属于抽象基类,抽象类用于为多个类型所共有的某些成员提供一个共享但可重写的机会,用抽象类作为其他类型所依赖的类型,以便达到运行时动态确定抽象类的具体类型,使类型之间的耦合关系具有一定的灵活性。

public abstract class A
{
    public abstract void Test( );
} 

public class B : A
{
    public override void Test( )
    {
        Console.WriteLine( "xxx" );
    }
}

public class Programe
{
    static void Main( string[] args )
    {
        A a = new B( ); //B重写了A的Test方法,然后隐式转换为A
        a.Test( );  //调用已经被B重写的A.Test方法 print xxx
    }
}

定义密封类

密封类主要用于防止被派生、被外部扩展。正因为它不能被派生,所以它不可能是基类。但它可以被实例化,这种特性在某些情况下与面向对象编程技术的灵活性和可扩展性是相抵触的,通常不建议使用密封类型。要声明密封类只需加上sealed形态修饰符,密封类的这种特性使其从不用作基类,因此在性能上对密封类成员的调用速度会略快。 

定义静态类

如果自定义的类是一个static的则应该将它的所有成员也定义为static。因为这样的类不能被实例化,逻辑上就不需要具备对象才可以访问的实例成员。

初始化对象

初始化就是为对象在内存上划分空间。使用new操作符调用构造函数可以在内存堆上分配一块空白区域,new在这块区域中创建出对象的this成员,并将对象的内存地址存入this中返回。这样就完成了一个对象的创建。 默认每个类都自动拥有一个无参版的构造函数,你可以不显示写出类的构造函数,这样,你的类将具备默认的无参构造函数,new对象时调用的正是这个默认无参的构造函数去初始化对象的。

public class Animal { public string name; } //没有定义自己的构造函数,这样它将获得默认无参的构造函数
Animal a = new Animal ( );

如果显示写出了无论是无参或有参版的构造函数,那么默认构造函数将会自动隐藏。

初始化成员

new调用构造函数,构造函数根据对象的所有成员所能占用的空间大小(位数)去计算出结果,然后在内存堆上划分对应大小的空间来存储这些成员。总结一下就是这样:new操作符调用构造函数 - 构造函数隐式地计算出类的成员和对象的成员所能占用的内存空间大小 - 再分配内存 - 在堆上存储this地址引用和初始化每一个成员,如果成员没有被你手动赋值,则对它们应用为其类型所对应的默认值(值类型总是=0,引用类型总是=null),如果成员已经被手动赋值,就将值放进内存中 -  进入构造函数方法体,如果有语句则执行,否则退出。也即在完成前面一系列工作后才会进入构造函数的方法体扫描代码。而初始化成员的值虽然由构造函数自动完成,但你也可以利用构造函数,在方法体内手动为成员赋一个你期望的值。引用类型的成员其默认值是null,null的变量被视为已赋值,所以你可以输出它而不会抛错,而未赋值的变量!=null,未赋值的变量不能访问,这两者有本质的区别。 为了确定构造函数(无论有参无参)初始化对象时确实计算并初始化了所有的成员并为它们赋了默认值,可参考下面简单的代码: 

public class Animal
{
    public string Name;
    public int FoodCount;
}

public class Programe
{
    static void Main( string [ ] args )
    {
        Animal a = new Animal( ); //流程:初始化Animal对象,分配内存,初始化成员字段name,然后进入构造函数代码体,代码体无可执行语句,退出。
        Console.WriteLine( a.Name == null ); //print true
        Console.WriteLine (a.FoodCount == 0); //print true
    }
public class Animal
{
    public string name;
    public Animal(string xName)
    {
        name = xName;
    }
}
Animal a = new Animal ( "蚂蚁" );
//流程:计算Animal对象的成员占用的空间大小,在堆上分配内存,初始化它的成员字段name,进入构造函数代码体,代码体存在一段为name字段赋新值的代码,执行后退出。

再来看一个例子,当类内部声明一个成员字段时显示为其赋了值,尔后又在构造函数中为其赋值,那么最终该成员字段的值是什么呢?按照以上的初始化流程应该很容易知道答案了:

public class Animal
{
    public string Name = "cat";
    public int FoodCount = 100;
    public Animal( )
    {
        Name = "dog";
        FoodCount = 200;
    }
}

public class Programe
{
    static void Main(string [ ] args)
    {
        Animal a = new Animal( );
        //流程:计算Animal对象的成员占用的空间大小,在堆上分配内存,初始化成员字段Name和FoodCount,然后进入构造函数代码体,代码体存在一段为两个字段赋新值的代码,执行后退出。
        //于是输出结果为:
        Console.WriteLine( a.Name ); //print dog
        Console.WriteLine( a.FoodCount ); //print 200
    }
}

新的初始化器

C#3.0中可以使用新语法来初始化对象,新的语法允许一个类在具备无参构造函数的前提下可以使用new操作符在初始化对象的同时为对象的成员赋值,如果类不存在无参构造函数则不能使用下面的语法。也即如果你的类定义了有参的构造函数,则必须显示指定一个无参版的构造函数,否则这种语法不能正常工作,因为它总是调用无参构造器来初始化对象和为对象的成员赋值。但是通常我们还是需要定义有参版构造函数,因为这样可以明确通知用户,我需要你提供哪些参数来初始化对象,而不是让用户利用初始化器来自己任意初始化成员。

public class Animal
{
    public string name;
    public Animal( )
    {

    }
}
Animal a = new Animal { name = "蚂蚁" }; //流程:计算Animal对象的成员占用的空间大小,在堆上分配内存,初始化成员字段name,然后进入{}代码体,代码体存在一段为name字段赋新值的代码,执行后退出。

new操作符的用途

调用构造函数初始化对象、对象初始化器(初始化对象时可初始化对象的成员)、初始化匿名对象、隐藏基类成员。

this和base关键字

this是new操作符隐式创建的对象的成员,这个成员存储的地址就是该对象,在类内部任何处都可以使用this访问到该对象。而base是派生类的基类,base是一个隐式创建的基对象,它存储基类对象在内存的地址。也即当你new一个Animal对象时,首先在内存中会创建Animal的基类object的实例,在派生类内部任何处都可以使用base访问到基对象,也即如果在派生类内部通过base访问基类成员,则说明调用base时已经隐式的在内存中创建了一个基类的实例。

匿名对象

使用关键字var可以创建一个匿名对象,这不需要类型做模板,如下所示: 

var obj = new { Name = "sam" , Age = 19 };

匿名对象的属性是只读,不可写的。可以将两个相等的匿名对象进行相互赋值。

var x = new { name="sam",age="19"};
var y= new { name = "leo", age = "20" };
x = y;
Console.WriteLine(x.name); //print leo

匿名对象也可以调用Equals方法,与具名对象(有类型的对象)的同名方法不一样。该方法会测试该匿名对象的每个属性的值与另一个匿名对象的属性的值是否相等(值相等或引用地址相等)且属性出现的先后顺序是否相同,如果为真则认为两个匿名对象是相等的。 

定义实例构造函数

构造函数在类型中定义,是类型的成员,new操作符正是调用构造函数来为对象做初始化工作的。构造函数可以是public、private、protected的,且其名称与类名必须相同,构造函数没有返回值,也不能return。

快速定义构造函数

键入ctor 连续按tab键两次,自动生成构造器。

构造函数重载

C#支持名称重复的函数名,重名的函数称为函数重载。但名称重复的函数必须具有不同的函数签名。对于构造函数来说,一旦构造函数重载,默认隐藏的无参构造函数就会被替换掉因而无法调用。

不同的方法签名

1.参数类型不同 | 2.参数个数不同 | 3.泛型类型参数不同 | 4.有无ref、out、params关键字的不同

外部代码块在调用同名的方法时会自动根据传递的参数的类型或参数个数来确定要调用的是哪一个方法。 

构造函数执行的顺序

如果实例化一个派生类,new操作符将逐层从上往下调用各个当前类型的基类的构造函数,默认情况下最开始调用的一定是System.Object类的Object构造函数。这是因为如果没有调用基类构造函数则派生类实例根本不可能得到基类的成员。

一个测试:

public class BaseClass
{
    public BaseClass() { Console.WriteLine("基类构造函数被执行"); }
}

public class Myclass : BaseClass//此处表示该类将从指定的基类派生
{
    public Myclass()
    {
        Console.WriteLine("派生类构造函数被执行");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Myclass myclass = new Myclass();
    }
}

输出结果为

指定基类构造函数

默认调用的是基类的无参版构造函数,也可以在函数括号后使用base关键字明确指定基类要执行的是哪个版本的构造函数。  

public class BaseClass
{
    public BaseClass( ) { Console.WriteLine ( "正在执行基类无参构造函数" ); }
    public BaseClass( string name ) { Console.WriteLine ( "正在执行基类带参构造函数" ); }
}

public class Myclass : BaseClass
{
    public Myclass( string name ) : base ( name ) //转接到基类带参构造函数上
    {
        Console.WriteLine ( "正在执行派生类构造函数" );
    }
}

抽象类也可以定义构造函数,但我们并不能用这个构造函数来初始化抽象实例,它只能由子类的构造函数去实现

public abstract class AbstractClass
{
    public string Name { get; set; }
    public AbstractClass(string name )
    {
        this.Name = name;
    }
}

public class A : AbstractClass
{
    //子类向抽象超类传递参数初始化抽象类的Name成员
    public A( string name ) : base( name ) { }
}

构造函数转接

可以在函数括号后使用this关键字把当前执行的构造函数转接到另一个构造函数上去。 

public class Test
{
    public int ID;
    public int Score;
    public Test( int result ) : this ( result , result )//转接到带参构造函数
    {
    }
    public Test( int id , int score ) { this.ID = id; this.Score = score; }
}

Test t = new Test ( 100 );

静态构造函数

静态构造函数的作用是初始化类的成员(被修饰为static)。与实例构造函数一样,每个类都具备一个默认的静态构造函数,也可以显示指定一个。但不能显示调用它,处于下面两种情况时会自动执行静态构造函数

初始化类成员的时期

1.当使用new操作符创建对象时

2.当访问静态成员时

静态构造函数只会执行一次

静态构造函数禁止指定权限修饰符,它作为类的成员只会被执行一次,也即new出对象时,或使用静态类访问成员时,静态构造函数会自动执行,但当再次new对象时,或再次访问成员时,静态构造函数就不会再执行了,这是因为类成员不同于对象成员,对象成员的所有权归对象所有,每创建一个对象都会在当前对象所在的内存区块中创建出对象的成员,多个对象之间各自维护着自己的成员,而类成员的所有权归属于类,是属于当前类的全局成员,只会在内存中创建一次,也即静态构造函数只可能被执行一次,在内存中为类创建好其成员后就不会再次执行。 

public class Test
{
    public static string none;
    static Test ( )  //实例构造函数使用public修饰、静态构造函数使用static修饰
    {
        Console.WriteLine ( "正在执行静态构造函数" );
    }
}

public class Programe
{
    static void Main ( string [ ] args )
    {
        Test.none="sam"//执行一次静态构造函数,打印:正在执行静态构造函数
        Test.none="leo"//这次访问成员时,静态构造函数不会执行,所以此次不会输出任何打印
    }
}

静态构造函数执行顺序

当new一个对象时,静态构造函数会先执行以便初始化类的成员,接着才会执行实例构造函数初始化对象的成员。这个顺序很好理解,毕竟类大于对象,初始化时也会按照这个原则执行。如果类是一个从属的派生类则new出其对象前会先执行它的静态构造函数,然后执行基类静态构造函数,完毕后再执行基类的实例构造函数、再执行派生类的实例构造函数。也即派生类的静态构造函数的执行顺序是从自身开始逐层往上,完毕后从基类开始逐层往下执行实例构造函数。 

public class Animal
{
    static Animal()
    {
        Console.WriteLine("正在执行Animal的静态构造函数");
    }
    public Animal()
    {
        Console.WriteLine("正在执行Animal的实例构造函数");
    }
}

public class Person : Animal
{
    static Person()
    {
        Console.WriteLine("正在执行Person的静态构造函数");
    }

    public Person()
    {
        Console.WriteLine("正在执行Person的实例构造函数");
    }
}

public class Programe
{
    static void Main(string[] args)
    {
        Person p = new Person();
    }
}

利用静态构造函数创建唯一的全局对象

因为静态构造函数只会执行一次,利用这个特点我们可以创建一个表示唯一性的、在任何地方都可以共享的对象

public class Animal
{
    public string CurrentName { getset; }
    static Animal ( )
    {
        Current = new Animal ( ); //在静态构造函数中实例化一个Animal对象,通过Current属性存取这个对象
    }
    public static Animal Current { getprivate set; }
}

//测试
Animal animal = Animal.Current;
animal.CurrentName = "碳基"
Animal animal2 = Animal.Current;
string s = animal2.CurrentName; //print 碳基

继承(派生)

继承即派生,它是面向对象编程的支柱。我们将一些基本的功能存放在基类中。子类通过自动继承获得基类定义的功能而无需重复编写已被基类实现的某些功能从而达到代码重用的目的。

继承规则

1.如果一个自定义的类没有显示设置它的基类,则默认其直接从system.Object派生。

2.一个类只能有一个基类

3.派生类继承除基类的构造函数和析构函数之外的所有成员(包括静态成员),但继承了基类的成员后,派生类并不一定能访问继承得来的成员,能不能访问取决于为那些成员定义的权限修饰符

2.类的访问权限遵从子类访问权限不能大于基类,要么其权限等于基,要么小于基。(类只有两种访问权限,要么internal要么public,所以很好记。基类如果是internal则子类不允许是public)

派生类可以获得基类的成员,比如基类是Animal,它有EatFood方法,它有两个派生类Cow和Chicken,它们通过继承自动获得EatFood方法:  

public class Animal 
{
    public void EatFood()
    {
    }
}

public class Cow : Animal
{
}

public class Chicken : Animal
{
}

static void Main(string[] args)
{
    Cow cow = new Cow();
    cow.EatFood();//自动获得基类的方法
    Chicken chicken = new Chicken();
    chicken.EatFood();//自动获得基类的方法
}

通过派生类的构造函数访问base代表的基对象的Name字段并初始化它。  

public class Animal
{
    public string Name { getset; }
}

public class Cow : Animal
{
    public Cow( string name )
    {
        base.Name = name;
    }
}

组合对象

Person从Animal继承后,不能再从其它类继承,比如一个表示人类智力的类Intelligence,Person为了获得Intelligence的成员,可以使用组合关系来模拟类似于派生的效果。实现很简单,也就是将Intelligence类型作为Person的一个字段或属性来存取。

基类与派生类之间的转换

基类对象不能转换为派生类型的对象,但派生类型的对象可以转换为基类型的对象。实际上我们不应该考虑转换问题而是将转换看成是赋值,记住这句话:赋值会测试赋值操作符左右两边的操作数其类型的等同性。现在假设有一个Person从Animal派生,看下面的解释: 

//基类型的对象为什么不能转换为派生类型的对象?
//要解决这个问题先看派生类对象转基类对象是怎么得来的

Person person = new Person { JobName = "programer" };

//流程:new操作符调用无参构造函数初始化person对象,这会先调用person的静态构造函数为person的静态成员划分内存空间 //再调用Animal的实例构造函数,计算Animal的成员所占用的空间大小,然后在堆上划分Animal的内存
//这样,Animal对象就出现在了内存中 //再再接下来才会开始计算person对象的成员所占用的空间,为person划分内存空间,最后进入{ }构造函数方法体,执行后退出
//最重要的一步来了,此时内存中的Animal对象将自身的地址拷贝给了person,也即person对象持有对Animal对象的引用,所以Person才能获得Animal的成员。            
//完成这一系列的操作后,看下面的代码

Animal animal = Person;

//当编译器发现=操作符时,它将判断左右两边的两个操作数其类型的等同性,看起来person和animal类型根本不一样,所以它们并不匹配 //但编译器发现person持有对animal的引用,要不它怎么获得基类的成员?所以立马将person指向的地址交给了animal。 //也即animal存储的是person指向的地址(包含内存中Animal对象的地址和自身的地址),但不能通过animal去访问地址指向的person的内容,但可以访问内存中的Animal对象的内容。 //要记住一点:animal确确实实存储了整个对person的引用,这样的引用完全合乎逻辑,行之有理,毫不违和。 //综上所述,以上的操作就可以完成

//现在来回答基类型的对象为什么不能转换为派生类型的对象?正是因为创建基类型对象后(Animal),内存中根本没有Person类型的对象被创建,并且基类型对象在内存中不持有对派生类型对象的引用,一个不存在的地址是无法交给另一个不同类型的,如下所示:

Animal animal = new Animal { Name = "sam" };
Person pp = ( Person ) animal;  //编译通过但运行时会抛出异常,能通过是因为显示转换类似于强转,告诉编译器我对我的行为负责,你不要管 //所以,今后看到引用类型之间的转换时,首先问自己,右边的对象在内存中是否持有对左边类型的某个对象的引用,有则可以转换,无则不能转换

只有在如下情况中才可能将基类型对象转换为派生类型的对象,总结起来一句话:基类型对象能转换为派生类型对象的办法是先将派生类型对象赋值给基类型的变量,再将基类型的变量转换为派生类型的对象。

Person p = new Person { JobName = "programer" };
Animal a = p; //编译器认为左右两边的操作数可以对等,因为p持有对内存中的Animal对象的引用,于是立即将p指向的地址交给a(包括自身的地址和内存中的Animal的地址),但a因为是Animal类型,所以它只能访问p地址指向的Animal的内容部分
Person newPerson = ( Person ) a; //显示转换后,a变成了Person类型,a本身存储着对p的引用,转换后自然可以访问p的内容
newPerson.JobName = "philosopher";
Console.WriteLine( p.JobName); //print philosopher

C# - 系统类和系统接口

泛型派生

泛型也是类类型,具有类类型的特点,所以可以派生,继承泛型的子类可以是泛型类或非泛型类。 

元组(Tuple)

关系数据库中每条记录(行)被称为元组,而每一列的数据被称为元数。在C#中,一组有关联的数据组合在一起就称为元组,每个操作数则称为元数。C#预定义了8个叫做Tuple的元组类。

Tuple
Tuple
Tuple
Tuple
Tuple
Tuple
Tuple
Tuple

它最多允许接收8个形参用以创建一个元组对象,元数是只读的,不可写,可按顺序从item1-item8访问元数。因为每一个T类型同时也可以是一个元组,所以Tuple中的每个元数也称为元组。

//创建一行数据

ValueTuple<int, string> v1 = ValueTuple.Create(1, "sam");          
Console.WriteLine(v1.Item1); //print 1
Console.WriteLine(v1.Item2); //print sam

//方式二
(int ID, string Name) v3 = (1, "sam");         
Console.WriteLine(v3.ID);//print 1
Console.WriteLine(v3.Name);//print sam

//创建二维表

//方式一
(ValueTuple<int, string> row1, ValueTuple<int, string> row2) table = ((1, "sam"), (2, "leo"));
Console.WriteLine(table.row1.Item1);//print 1
Console.WriteLine(table.row1.Item2);//print sam
Console.WriteLine(table.row2.Item1);//print 2
Console.WriteLine(table.row2.Item2);//print leo

//方式二
(int ID, string Name) row1= (1, "sam");
(int ID, string Name) row2 = (2, "leo");
var table2 = (row1, row2);
Console.WriteLine(table2.row1.ID);//print 1
Console.WriteLine(table2.row1.Name);//print sam
Console.WriteLine(table2.row2.ID);//print 2
Console.WriteLine(table2.row2.Name);//print leo

Tuple应用场景

1.函数只能返回一个某种类型的变量,当某函数需要返回多个变量时,可使用out、ref修饰传引用或传输出的参数,还可以让函数直接返回一个值元组 

static Tuple<int , string , float , Tuple<string>> Test( )
{
    return Tuple.Create( 1 , "sam" , 1.75f , Tuple.Create( "favit:none" ) );
}

2.当函数需要接收一系列参数时,不需要params而以元组作为替代 

static void Test(Tuple<int , string , float , Tuple<string>> tuple )
{
    int ID = tuple.Item1;
    string name = tuple.Item2;

}
static void Main(string [ ] args)
{
    Test( Tuple.Create( 1 , "sam" , 1.75f , Tuple.Create( "favit:none" ) ) );
}

值元组(ValueTuple)

值元组是C#7.0的新类,framework4.7已经内置,如果是4.7以下版本则可以通过NuGet下载。

ValueTuple
ValueTuple
ValueTuple
ValueTuple
ValueTuple
ValueTuple
ValueTuple
ValueTuple
ValueTuple

区别于Tuple是引用类型的泛型,ValueTuple是结构类型的泛型,而且值元组不但可读可写,它还有新的语法糖使代码得以简化。声明一个值元组类型,可以使用ValueTuple的Create工厂方法,但用语法糖将进一步简化代码量。

//创建一行数据

ValueTuple<int, string> v1 = ValueTuple.Create(1, "sam");          
Console.WriteLine(v1.Item1); //print 1
Console.WriteLine(v1.Item2); //print sam

//方式二
(int ID, string Name) v3 = (1, "sam");         
Console.WriteLine(v3.ID);//print 1
Console.WriteLine(v3.Name);//print sam

//创建二维表

//方式一
(ValueTuple<int, string> row1, ValueTuple<int, string> row2) table = ((1, "sam"), (2, "leo"));
Console.WriteLine(table.row1.Item1);//print 1
Console.WriteLine(table.row1.Item2);//print sam
Console.WriteLine(table.row2.Item1);//print 2
Console.WriteLine(table.row2.Item2);//print leo

//方式二
(int ID, string Name) row1= (1, "sam");
(int ID, string Name) row2 = (2, "leo");
var table2 = (row1, row2);
Console.WriteLine(table2.row1.ID);//print 1
Console.WriteLine(table2.row1.Name);//print sam
Console.WriteLine(table2.row2.ID);//print 2
Console.WriteLine(table2.row2.Name);//print leo

应用值元组的场景

//返回一个值元组类型
static (int ID, string Name) GetData()
{
    return (1, "sam");//值元组实例
}

//接收一个值元组类型
static void GetData((int ID,string Name) valueTuple)
{
    int id = valueTuple.ID;
    string name = valueTuple.Name;
}

泛型方法

当泛型类所定义的类型形参不满足方法的需求时,可以考虑将方法定义成泛型方法,具有自己独有的类型形参的方法称为泛型方法。既然泛型方法也是一种方法,方法具有的一切特征,泛型方法也都具备,是共通的。

public class Unknown<T>
{
    //非泛型方法
    public static void Take1 ( T x ) { }
    //泛型方法
    public static void Take2<X> ( X x , T t ) { }
}

public class Anonymous
{
    //泛型方法
    public static void Take2<XT> ( X x , T t ) { }
}

泛型方法重载

确定泛型方法重载是根据类型参数的个数不同或方法参数的不同。

调用泛型方法

调用泛型方法时,如果该方法的类型形参出现在方法的参数列表中则可以不指定类型实参,因为编译器会自行对调用方法时圆括号中的参数类型进而类型推断从而确定类型形参的实参类型,但假如类型形参没有出现在方法的参数列表中,此时必须显示地指定方法的类型实参。 

public class Anonymous
{
    public static void Take<T>(T x, T y) { }
    public static void PickUp<T>(string z) { }
}

public class Programe
{
    static void Main(string[] args)
    {
        Anonymous.Take("sam", "korn"); //Take的T形参出现在了圆括号的参数列表中,调用方法时可以不指定类型实参,编译器自动推断
        Anonymous.PickUp<int>("sam"); //PickUp的T形参没有出现在圆括号的参数列表中,调用方法时必须指定类型实参
    }
}

泛型的协变与逆变

C#仅允许泛型接口和泛型委托支持类型参数的可变性(协变、逆变),什么意思呢?先看一个泛型接口 

public interface IFication<T>
{
    void Test(T t);        
}

public class Animal : IFication<Animal>
{
    void IFication<Animal>.Test(Animal t) { }
}

public class Person :Animal, IFication<Person>
{
    void IFication<Person>.Test(Person t) { }
}

public class Programe
{
    static void Main(string[] args)
    {
        Animal a = new Animal();
        IFication<Animal> aFication = a;
        IFication<Person> pFication = aFication; //提示无法进行转换
        //声明Animal变量时,在栈上划分32位的空间以便存储Aniaml对象在堆上的地址
        //使用new操作符调用Animal构造函数,构造函数发现Animal实现了IFication接口,
        //于是将基于Animal实现的IFication版本的接口实例存储在内存中,并将接口的地址交给Animal变量持有
        //构造函数计算Animal成员所能占用的空间大小,然后初始化成员
        //将Animal对象隐式转换为IFication接口
        //试图将变量aFication转换为IFication类型
        //右边操作数只持有基于Animal实现的IFication版本的接口实例的地址和自身的地址
        //它并不持有左边操作数的类型在内存中的地址,所以这个转换是失败的
    }
}

两个泛型接口或泛型委托中的类型参数如果存在派生关系,那么理论上是可以实现相互转换的,而这种转换需要你自己做出判断后,使用out、in关键字去声明泛型接口/委托的类型参数,out和in关键字用于声明泛型接口/委托的类型参数究竟是支持协变还是支持逆变,声明后编译器就会通过,不再提示错误。

协变out输出参数), 表示接口的类型参数可以转换为其父类型子转父
逆变in,(输入参数),表示接口的类型参数可以转换为其子类型父转子 接下来就是判断自己写出的泛型接口/委托究竟应该支持协变还是支持逆变,判断方式是根据泛型接口的方法成员的返回类型或方法签名(泛型委托的返回类型或签名)进行确定的
//-------------------支持逆变的三种判断方式-------------------
//1.接口方法将T用于方法的参数,不用于方法的返回类型
//2.接口的方法将另一个支持T协变的接口用于方法的参数,不用于方法的返回类型
//3.接口的方法将另一个支持T逆变的接口用于方法的返回类型,不用于方法的参数

//-------------------支持协变的三种判断方式-------------------
//1.接口方法将T用于方法的返回类型,不用于方法的参数
//2.接口的方法将另一个支持T协变的接口用于方法的返回类型,不用于方法的参数
//3.接口的方法将另一个支持T逆变的接口用于方法的参数,不用于方法的返回类型 综上所述,所以在上面的例子中,泛型接口 IFication中的Test方法因为是接收了输入参数T,所以该接口应该支持逆变,在上面的例子中只需要将in关键字应用在T前面就可以顺利实现转换。 public interface IFication<in T>
{
    void Test(T t);        
}

那接口中可能不止一个方法,假如其他方法将T作为了返回类型又怎么办呢?因为Test是接收T而非输出T,所以该接口支持逆变,现在多了一个输出T的方法,我们就不知道该如何为T定义in或out了!其实很简单,如果这种情况存在,那说明你的泛型接口不支持协变也不支持逆变,编译器会提醒你错误。也即泛型接口/委托能不能支持协变、逆变与方法有关,首先我们是定义方法而不是专注于去设计协变逆变,能支持就支持,不能支持就拉倒,就这么简单。

//以下三种写法都支持T类型的逆变:

//第一种:
public interface IFication<in T>
{
    void Test ( T t );
}

//第二种:
public interface IFication<in T>
{
    void Test ( IRecation<T> );
}

public interface IRecation<out X>
{
    X ReTest ( );
}

//第三种:
public interface IFication<in T>
{
    IRecation<T> Test ( );
}

public interface IRecation<in X>
{
    void ReTest ( X x );
}

IFication a = new Animal ( );
IFication p = a; //以下三种写法都支持T类型的协变:

//第一种:
public interface IFication<out T>
{
    T Test ( );
}

//第二种:
public interface IFication<out T>
{
    IRecation<T> Test ( );
}

public interface IRecation<out X>
{
    X ReTest ( );
}

//第三种:
public interface IFication<out T>
{
    void Test ( IRecation<T> t );
}

public interface IRecation<in X>
{
    void ReTest ( X x );
}

IFication p = new Person ( );
IFication a = p;

画图秒懂:

参考了这篇大神的文章:.NET 4.0中的泛型协变和逆变(大神去世了,愿一路走好)。

泛型约束

泛型的类型参数可以指向任何类型,假如要处理T类型,但T类型是不确定的类型,这就没法调用其构造函数创建其实例了。所以C#提供了泛型约束,用以指明T类型可用的类型范围。可以为类型形参指定多个约束,约束总是放在泛型类型声明的结尾处,约束以where T : 开头,每个约束以逗号隔开。可以指定非密封类的约束如下::

1.类类型约束:如Animal,必须出现在约束的最开始且只能有一个,不能同时指定class或struct
2.引用类型约束:class,必须出现在约束的最开始且只能有一个,不能同时指定Animal或struct
3.值类型约束:struct,必须出现在约束的最开始且只能有一个,不能同时指定Animal或class
4.无参构造器约束:new( ) [如果不指定构造器约束则编译器不允许这样的类型形参调用构造函数] 2.接口约束:如ICompareble,可指定多个 public class Stack<TWSXYHP>
    where T : IComparable // T类型必须实现IComparable接口
    where S : struct // S类型必须是结构
    where X : IComparablenew()    // X类型必须实现IComparable接口且拥有无参构造函数
    where Y : Animal // Y类型必须是Animal类或派生自Animal
    where H : class // H类型必须是一个引用类型
    where P : T //P类型必须派生自T
{

}

//一个容易出错的例子
public class Template<T> { }

public class List<XY>
    where X : Template<Y>
{
    public X GetX ( )
    {
        return new Template<Y> ( ); //提示:无法将Template隐式转换为X
    }
}

// X 可能是一个Template的派生,假设用户传递的是子类,则X=子,而new Template是直接创建new Template的实例(父),所以这个转换是失败的

如果基类泛型的某个类型形参有一个约束,那么从其派生的泛型类在继承基类泛型时提供的类型参数也必须得到与基类相同的约束,在保证与基类一致的约束前提下,派生泛型可以继续为类型参数增加更多的约束。

如果泛型方法是一个虚方法,当该方法为某个类型形参指定约束后,派生类如果重写了此方法,则不能再次对类型形参提供约束,因为只能修改基类方法的实现,而不能修改为方法所下的定义,约束已经在基类方法中完成定义,没必要重复为方法定义两次约束。但new隐藏基类的泛型方法可以提供新的约束,因为该方法是派生类自己的成员而非基类,所以不受限制。

泛型的CIL表示 

public class Stack<T>
{
    T [ ] Items;
}

Stack ' 1表示该泛型的元数是1,泛型中的类型参数以!表示

泛型静态构造函数

与普通类型的静态构造函数的逻辑是一致的,但需要注意 Animal和Animal不是相同的类型,所以Animal的静态成员(类成员)与Animal的静态成员是相互独立的版本,当访问这两个不同类型的成员或实例化它们时,它们的静态构造函数自然会各自独立执行而不是只执行一次。

泛型的本地代码

定义泛型时定义的是个类型模板,它并不能用来执行并创建出对象。这就像把类进行了一次抽象,运行时先要将模板转化为真正可执行的泛型类型,接着才能实例化泛型。运行时会根据类型参数(值类型或引用类型)的不同而创建不同的本地代码。,本地代码才使真正可执行的、能创建出对象的代码。

//现在有一个Stack的泛型模板,当使用它时会提供类型实参
//在运行时会根据类型实参的不同创建不同的本地代码   
public class Stack<T>
{
    public T x;
    public T Test (T t )
    {
        return t;
    }
}

//如果是值类型,那么不同的值类型会创建不同的本地代码
//所谓本地代码就是如下所示的、真正可用于实例化泛型的类型代码
//比如使用Stack时提供的类型实参是Stack,则运行时生成的本地代码如下
public class Stack<int>
{
    public int x;
    public int Test ( int t )
    {
        return t;
    }
}

//比如使用Stack时提供的类型实参是Stack,则运行时生成的本地代码如下
public class Stack<long>
{
    public long x;
    public long Test ( long t )
    {
        return t;
    }
}

//比如使用Stack时提供的类型实参是Stack等所有的引用类型,则运行时生成的本地代码都是objec引用,t如下
//这样做的好处不言而喻,不需要为Stack、Stack等类似的引用类型单独生成不同的本地代码,避免本地代码爆炸
//实例化泛型类型时,object会指向具体的泛型实例在内存堆上的地址
public class Stack<object>
{
    public object x;
    public object Test ( object t )
    {
        return t;
    }
}

使用泛型

现在假设我们需要写一个能提供排序算法的MySort类,它的GetSort方法可以对int类型的数组执行排序。我们假设后来又需要对double类型的数组进行排序,那么我们唯一的办法就是重载GetSort方法,但这样的话就有两个版本的GetSort,它们的算法逻辑却是相同的,重复编写相同的代码是很蠢的事情,此时就可以考虑使用泛型了。  

namespace ConsoleApp1
{
    //将数组元素按从小到大排序
    public class MySort
    {
        public void SetSort ( int [ ] intAry )
        {
            int aryLength = intAry.Length;

            for ( int i = aryLength - 1 ; i > 0 ; i-- )
            {
                for ( int z = aryLength - 1 ; z > 0 ; z-- )
                {
                    bool result = intAry [ z ].CompareTo ( intAry [ z - 1 ] ) < 0;
                    if ( result )
                    {
                        int temp = intAry [ z - 1 ];
                        intAry [ z - 1 ] = intAry [ z ];
                        intAry [ z ] = temp;
                    }
                }
            }
        }
    }
    class Program
    {
        static void Main ( string [ ] args )
        {
            MySort m = new MySort ( );
            int [ ] a = { 2 , 900 , 1 , 200 , 1000 , 2000 , 500 , 20000 , 5 , 300 , 0 , };
            m.SetSort ( a );
            foreach ( var item in a )
            {
                Console.WriteLine ( item );
            }
        }
    }
} 非泛型的排序算法 namespace ConsoleApp1
{
    public class MySort<Twhere T : IComparable
    {
        public void SetSort ( T [ ] TAry )
        {
            int aryLength = TAry.Length;

            for ( int i = aryLength - 1 ; i > 0 ; i-- )
            {
                for ( int z = aryLength - 1 ; z > 0 ; z-- )
                {
                    bool result = TAry [ z ].CompareTo ( TAry [ z - 1 ] ) < 0;
                    if ( result )
                    {
                        T temp = TAry [ z - 1 ];
                        TAry [ z - 1 ] = TAry [ z ];
                        TAry [ z ] = temp;
                    }
                }
            }
        }
    }

    class Program
    {
        static void Main ( string [ ] args )
        {
            MySort<double> m = new MySort<double> ( );
            double [ ] a = { 2 , 900 , 1 , 200 , 1000 , 2000 , 500 , 20000 , 5 , 300 , 0 , };
            m.SetSort ( a );
            foreach ( var item in a )
            {
                Console.WriteLine ( item );
            }
        }
    }
} 泛型排序算法

 

特性(Attribute)

特性也是一种类型,但用法与普通的类型有区别,使用[AttributeName(……)]将特性置于程序集、类型、结构、枚举、接口、委托、事件、字段、属性、方法、方法参数、泛型类型参数、返回值、程序模块(为了便于描述,暂且统称这些东西为特性的目标)等等的声明之前,用于对特性目标进行描述,编译器可以读取特性并自动根据特性的内部实现对特性目标采取相应的动作。 

内置特性

1.Obsolete ( string message , bool IsError )

描述某个对象不再被使用,提醒编码人员的注意。IsError可选,默认false。如果IsError为true则编译器会禁止对该对象的运行并在错误列表中显示参数指定的message,如果IsError为false则只是发生警告,不会禁止对该对象的运行。 [Obsolete("此类已被抛弃,请使用XAnimal类创建实例",true)]
public class Animal //Animal是Obsolete的特性目标
{
    public string name="蚂蚁";
}

Animal a = new Animal ( );
Console.WriteLine(a.name );

 

2.AttributeUsage ( AttributeTargets , AllowMultiple = bool , Inherited = bool )

用于定义在自定义的特性类上,

ValidOn:AttributeTargets枚举:用于指定特性只能运用在什么对象上,比如程序集、类型、构造函数、委托等等……

AllowMultiple=bool:指定自定义的特性是否可以在一个特性目标上被运用多次。

Inherited=bool:假如某父类和子类同时运用了某个特性,Inherited指定运用到子类上的特性是否可以持有对运用到父类上的特性的访问,如果Inherited=true则必须保证AllowMultiple=true,否则抛错。

AttributeTargets.Assembly; // 特性可以应用于程序集
AttributeTargets.All; // 特性可以应用于任何特性的目标
AttributeTargets.Class;
AttributeTargets.Constructor;
AttributeTargets.Delegate;
//略……

下面是Inherited的例子:

[AttributeUsage(AttributeTargets.All, AllowMultiple = true,Inherited=true)]
public class A : Attribute 
{
    public string Name;
    public A(string str) { this.Name = str; } 
}

[A("xxx")]
public class Animal { }
    
[A("yyy")]
public class Person:Animal { }


class Program
{
    static void Main(string[] args)
    {
        Type t = typeof(Person);
        var attri = t.GetCustomAttributes(true); //获取Person的特性集合
        Console.WriteLine((attri[0] as A).Name); //yyy
        Console.WriteLine((attri[1] as A).Name); //xxx
        //如果Inherited=false 则attri数组只有一个实例,只返回yyy,不会返回父类的A.Name
        //另,GetCustomAttributes方法的参数是一个额外的开关,如果传递false,则不会迭代到父类的A特性
    }
}

3.assebly、module、return

打开项目的properties可以看到一个AsseblyInfo.cs文件,此文件大量运用了内置的assembly特性用于描述程序集的信息,例如公司、产品名,版本号等。注意,assebly和module这两个特性必须出现在所有using引用之后。assebly应出现在所有命名空间之前,而module应出现在所有类的声明之前。最后,return特性应出现在方法之前。下面是return特性的例子:

public class A : Attribute { public A(string description) { } }

public class Animal
{
    [return: A("如果没有提供参数,将返回默认值sam")]
    public string Name()
    {
        return "sam";
    }

    public string Name(string name)
    {
        return name;
    }
}

4.Flags

这是一个专门应用在枚举上的特性,表示这是一个位标志枚举,可以利用这个特性使一组枚举值可以存储在一个变量中。参看本页的枚举一节。

自定义特性

使自定义的特性类从Attribute派生就可以了。从Attribute派生的类都自动隐性的添加了Attribute后缀,但声明特性或运用特性时可以不显示添加Attribute后缀,只有在以内联的方式访问特性时才需要提供后缀,如:typeof(AnalysisAttribute)

public class Analysis : Attribute
{
    public string Description { getset; }
    public Analysis ( string message )
    {
        Description = message;
    }
}

[Analysis ( "此类是密封的,不能继承" )]
public sealed class Philosopher
{

}

public class Programe
{
    static void Main ( string [ ] args )
    {
        Type type = typeof ( Philosopher ); //获取Philosopher的Type表示
        Attribute attri= type.GetCustomAttribute ( typeof ( Analysis ) ); //在Philosopher的Type表示上获取Analysis特性
        Analysis analysis = attri as Analysis;
        Console.WriteLine ( analysis.Description );
    }
}

特性可以以逗号分隔写在一行,但合并的写法不适用于返回值、程序集和模块。

public class A : Attribute { }
public class B : Attribute { public SwitchAlias(string country) { } }

public class Animal
{
    [A]
    [B("chinese")]
    public string Name { get; set; }
}

public class Animal
{
    [A, B("chinese")]
    public string Name { get; set; }
}

 

C# - 学习总目录 

aa