泛型Generic
基本定义
泛型无处不在,是和接口一样重要的存在。
- 为什么需要泛型:避免成员膨胀或者类型膨胀。
- 正交性:泛型类型(类、接口、委托…)、泛型成员(属性、方法、字段…)
- 类型方法的参数推断
- 泛型与委托、lambda表达式
泛化和具体化是相对的,所有泛型编程实体都不能直接拿来使用的,必须要经过特化之后才能拿来使用。
泛型类示例
示例:小商店店主,一开始只卖苹果,卖苹果会赠送盒子,用来包装苹果。如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "Red" };
Box box = new Box() { Cargo = apple };
Console.WriteLine(box.Cargo.Color);
}
}
class Apple
{
public string Color { get; set; }
}
class Box
{
public Apple Cargo { get; set; }
}
}
随着商店经营越来越好,卖的东西也越来越多的,商店开始卖书了。卖书会赠送盒子,用来包装书本。此时就面临一个问题,因为苹果已经有一个Box类,书也要一个盒子,就需另外添加一个BookBox类。如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "Red" };
AppleBox box = new AppleBox() { Cargo = apple };
Console.WriteLine(box.Cargo.Color);
Book book = new Book() { Name="New Book"};
BookBox bookBox = new BookBox() { Cargo = book };
Console.WriteLine(bookBox.Cargo.Name);
}
}
class Apple
{
public string Color { get; set; }
}
class Book
{
public string Name { get; set; }
}
class AppleBox
{
public Apple Cargo { get; set; }
}
class BookBox
{
public Book Cargo { get; set; }
}
}
此时代码已经出现类型膨胀的问题了,因为随着商店产品越来越多,需要准备的盒子就越来越多,这样就造成box类极度膨胀,使得代码非常不好维护。对代码进行改写,只有一种盒子的方案,如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "Red" };
Book book = new Book() { Name="New Book"};
Box box1 = new Box() { Apple = apple };
Box box2 = new Box() { Book = book };
Console.WriteLine(box1.Apple.Color);
Console.WriteLine(box2.Book.Name);
}
}
class Apple
{
public string Color { get; set; }
}
class Book
{
public string Name { get; set; }
}
class Box
{
public Apple Apple { get; set; }
public Book Book { get; set; }
}
}
这时又出现了另外一个叫成员膨胀的问题,如果商店有1000种商品,那么在Box中就需要1000个对应的属性,而且每次这1000个属性中,只有一个会被使用,其余都用不着,而且每次增添新商品时,都需要修改Box类,增加新的属性,反之,如果商店不卖某个商品,还要在Box类中删除对应的商品属性。如果忘记向Box类中添加或者删除属性,那么代码都会存在BUG。进行另外一个方式的改进,在Box类中申明属性时,不要指定具体类型,使用Object类型属性,因为dotnet是单根的类型系统。如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "Red" };
Book book = new Book() { Name="New Book"};
Box box1 = new Box() { Cargo = apple };
Box box2 = new Box() { Cargo = book };
Console.WriteLine((box1.Cargo as Apple)?.Color);
}
}
class Apple
{
public string Color { get; set; }
}
class Book
{
public string Name { get; set; }
}
class Box
{
public Object Cargo { get; set; }
}
}
这种方法不能直接用”.”操作符调用盒子里面物品的属性,即”box1.Cargo.”后面是不会显示Color属性,需要对box1.Cargo进行强制类型转换或者使用as,即”box1.Cargo as Apple)?.Color”,这样显得十分复杂。使用泛型,会解决当前的调用复杂问题。把Box普通类改成泛型类,变成Box
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Apple() { Color = "Red" };
Book book = new Book() { Name="New Book"};
Box<Apple> box1 = new Box<Apple>() { Cargo = apple };
Box<Book> box2 = new Box<Book>() { Cargo = book };
Console.WriteLine(box1.Cargo.Color);
Console.WriteLine(box2.Cargo.Name);
}
}
class Apple
{
public string Color { get; set; }
}
class Book
{
public string Name { get; set; }
}
class Box<TCargo>
{
public TCargo Cargo { get; set; }
}
}
泛型接口示例
以上示例完美解决了类型膨胀和成员膨胀的问题,再次举一个泛型接口的示例,如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Student<int> stu = new Student<int>();
stu.Id = 101;
stu.Name = "Tim";
//随着学校越来越大,int类型不够装所有学生的ID,改用无符号的长整型作为学生的ID
Student<ulong> stu2 = new Student<ulong>();
stu2.Id = 100000000000000001;
stu2.Name = "Jerry";
}
}
/// <summary>
/// 泛型IUnique接口
/// </summary>
/// <typeparam name="T"></typeparam>
interface IUnique<T>
{
T Id { get; set; }//Id为成员属性
}
/// <summary>
/// 泛型Student类,在实现泛型IUnique接口时,Student类也必须变为泛型类
/// </summary>
/// <typeparam name="T"></typeparam>
class Student<T> : IUnique<T>
{
public T Id { get; set; }
public string Name { get; set; }
}
}
以上在申明Student时,实现了IUnique接口,Student类也就成为了泛型类,还有一种实现泛型接口的方法为,当Student类在实现泛型IUnique接口时,实现的是一个特化之后的泛型接口,此时类就不再是泛型类了,如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Student stu = new Student();
stu.Id = 100000000000000001;
stu.Name = "Jerry";
}
}
/// <summary>
/// 泛型IUnique接口
/// </summary>
/// <typeparam name="T"></typeparam>
interface IUnique<T>
{
T Id { get; set; }//Id为成员属性
}
/// <summary>
/// Student类,实现特化后的泛型IUnique接口
/// </summary>
/// <typeparam name="T"></typeparam>
class Student : IUnique<ulong>
{
public ulong Id { get ; set ; }
public string Name { get; set; }
}
}
泛型集合示例
泛型之所以无处不在,在编程影响深刻,是因为有两个原因:
- 泛型具有良好的正交性
- 在dotnet framework中几乎所有常用的数据结构都是泛型的
编程一定是在处理数据的,大量的数据都是存储在各种各样的集合当中的,常用的集合有:数组,列表,链表,字典等,这些集合都是泛型的,他们的基接口和基类也是泛型的。
using System;
using System.Collections.Generic;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
IList<int> list = new List<int>();
//IList<int>为带有一个类型参数的泛型接口,List<int>为带有一个类型参数的泛型类,其背后维护着一个数组,由于数组的长度是不可以改变的,而List的长度是可以改变的,所以List也叫动态数组。
for (int i = 0; i < 100; i++)
{
list.Add(i);
}
foreach (var item in list)
{
Console.WriteLine(item);
}
}
}
}
带有多个类型参数的泛型接口和泛型类示例
很多泛型类型都带有不止一个类型参数,如IDictionary
using System;
using System.Collections.Generic;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
IDictionary<int, string> dict = new Dictionary<int, string>();
dict[1] = "Tim";
dict[2] = "Tom";
Console.WriteLine($"Student #1 is {dict[1]}");
Console.WriteLine($"Student #2 is {dict[2]}");
}
}
}
泛型方法示例
示例:有两个整型数组,将他们像拉拉链一样组合在一起,方法的名称叫做Zip(),如下:
using System;
using System.Collections.Generic;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
int[] a1 = { 1,2,3,4,5};
int[] a2 = { 1,2,3,4,5,6};
double[] a3 = { 1.1,2.2,3.3,4.4,5.5};
double[] a4 = { 1.1,2.2,3.3,4.4,5.5,6.6};
var result = Zip(a1,a2);
Console.WriteLine(string.Join(",",result));
}
static int[] Zip(int[] a, int[] b)
{
int[] zipped = new int[a.Length + b.Length];
int ai = 0, bi = 0, zi = 0;
do
{
if (ai<a.Length)
{
zipped[zi++] = a[ai++];
}
if (bi < b.Length)
{
zipped[zi++] = b[bi++];
}
} while (ai<a.Length||bi<b.Length);
return zipped;
}
}
}
此时Zip方法无法运行double类型的参数,即Zip(a3,a4)编译会报错。想要运行通过:
第一个办法为将Zip方法重载,形参和返回值类型都为double类型, 但是这样又会出现成员膨胀的问题,这样的方法成员膨胀,会比属性成员膨胀更危险。因为在这两个方法中有绝大部分逻辑是重复的,如果后续要修改或升级其中方法的逻辑,那么另一个方法的逻辑也势必会需要修改或升级,一旦有一个重载的版本忘记修改或升级,那么程序就会存在隐藏的BUG。未来如果要处理float数组、long数组等更多类型的数组,那么就要不断地进行copy parse,显然这样是不行的。
第二个办法就是使用泛型方法,如下:
using System;
using System.Collections.Generic;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
int[] a1 = { 1,2,3,4,5};
int[] a2 = { 1,2,3,4,5,6};
double[] a3 = { 1.1,2.2,3.3,4.4,5.5};
double[] a4 = { 1.1,2.2,3.3,4.4,5.5,6.6};
var result = Zip(a1,a2);
var result2 = Zip(a3,a4);
Console.WriteLine(string.Join(",",result));
Console.WriteLine(string.Join(",", result2));
}
/// <summary>
/// Zip<T>()为泛型方法,
/// </summary>
/// <typeparam name="T">T为类型参数</typeparam>
/// <param name="a">T类型的数组形参a</param>
/// <param name="b">T类型的数组形参b</param>
/// <returns>返回T类型的数组zipped</returns>
static T[] Zip<T>(T[] a, T[] b)
{
T[] zipped = new T[a.Length + b.Length];
int ai = 0, bi = 0, zi = 0;
do
{
if (ai<a.Length)
{
zipped[zi++] = a[ai++];
}
if (bi < b.Length)
{
zipped[zi++] = b[bi++];
}
} while (ai<a.Length||bi<b.Length);
return zipped;
}
}
}
泛型委托示例
示例:Action<>泛型委托,只能引用没有返回值的方法,如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Action<string> a1 = Say;
a1.Invoke("Tim");
Action<int> a2 = Mul;
a2(1);
}
static void Say(string str)
{
Console.WriteLine($"Hello,{str}!");
}
static void Mul(int x)
{
Console.WriteLine(x*100);
}
}
}
<br />示例:Func<>泛型委托,引用有返回值的方法,如下:
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Func<int,int,int> func1 = Add;
Console.WriteLine(func1.Invoke(100, 200)) ;
Func<double, double, double> func2 = Add;
Console.WriteLine(func2(100.1,200.2));
}
static int Add(int a,int b)
{
return a + b;
}
static double Add(double a,double b)
{
return a + b;
}
}
}
示例:泛型委托和Lambda表达式的结合使用。
using System;
namespace HelloGeneric
{
class Program
{
static void Main(string[] args)
{
Func<int,int,int> func1 = (a, b)=> { return a + b; };
Console.WriteLine(func1.Invoke(100, 200)) ;
Func<double, double, double> func2 = (a, b)=> { return a + b; };
Console.WriteLine(func2(100.1,200.2));
}
}
}
partial类
基本定义
C#编译器允许我们把一个类的代码分成两部分或者多部分来编写,每一个部分都有自己的逻辑单元,而且每个部分都能以自己的进度进行版本更新,最终每个部分合起来还是同一个类,还有附带的好处就是类名也是相同的。
- 为何需要partial类:因为它可以减少类的派生,减少派生的复杂性
- partial类与Entity Framework
- partial类与Windows Forms,WPF,ASP.NET Core
使用Entity Framework访问数据库示例
示例:使用Entity Framework访问数据库。首先管理NuGet程序包,添加EntityFramework框架。然后在SQL Server中添加Bookstore表,如下:
然后在当前程序集(Bookstore.Client)下新建ADO.NET实体数据模型项目,并新建连接,连接属性设置如下:
并选择Book作为模型中包括的数据库对象。如下:
此时就得到了一个映射好的Book类,如下:
测试并连接数据库,打印每本书的名字,如下:
using System;
namespace Bookstore.Client
{
class Program
{
static void Main(string[] args)
{
var dbContext = new BookstoreEntities();
var books = dbContext.Books;
foreach (var book in books)
{
Console.WriteLine(book.Name);
}
}
}
}
此时对Books右键转到定义,对Book右键转到定义时,可以看到Book类的所有属性,如下:
namespace Bookstore.Client
{
using System;
using System.Collections.Generic;
public partial class Book
{
public int ID { get; set; }
public string Name { get; set; }
public Nullable<double> Price { get; set; }
}
}
现在有一个新需求,就是让Book类能够自动报告自己的状态,即增加一个Report()方法,能报告ID,Name以及price。如果在EntityFramework框架自动生成的Book类中增加Report()方法的话,且如果数据库有改动,刷新BookstoreModel时,此类会重新自动生成,所添加的Report()方法会被自动丢弃。
解决方法:由于Book类是一个partial类,此时应该新建一个类文件(类名BookPart2),将Book类的另一部分写在这个类文件中,需注意新写的BookPart2类文件,名称空间、类名应与之前的Book类一致。如下:
namespace Bookstore.Client
{
using System;
using System.Collections.Generic;
public partial class Book
{
public string Report()
{
return $"#{this.ID} Name:{this.Name} Price:{this.Price}";
}
}
}
这样主程序Main就也能使用book的Report()方法了: Console.WriteLine(book.Report());
如果在SQL Server中的Book表中,新增加一列,保存后,执行编辑前200行,回到VS的BookstoreModel中右键从数据库更新模型,并保存当前代码,这时回到Book类中可以看到Book类多了一个Author属性。继续运行程序时,Report()方法一样可以被调用。
WinForm中partial类示例
新建winform程序,Form1窗体类其实就是一个partial类,他被分成了两部分,一部分为UI设计部分,一部分为后台逻辑部分,两部分都是由C#语言编写,如下:
namespace WindowsFormsApp1
{
partial class Form1
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows 窗体设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
}
#endregion
}
}
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}
WPF中partial类示例
新建WPF程序,MainWindow窗体类也是一个partial类,他被分成了两部分,一部分为由Xaml语言写的UI设计部分,一部分为由C#语言写的后台逻辑部分,如下:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
</Grid>
</Window>
using System.Windows;
namespace WpfApp1
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
ASP.NET Core中partial类示例
新建ASP.NET Core Web应用程序(模型视图控制器),在Views→Home→Index.cshtml中有两部分代码,一部分是C#语言写的,一部分是HTML语言写的,他们是一个partial类的一部分,最终会被编译成C#代码与后台类合并在一起,如下:
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
partial虽然在实际很少直接使用,但是它是现代.net编程的基石之一。
枚举类型
基本定义
- 本质:人为限定取值范围的整数
- 整数值的对应
- 用法:比特位式用法
示例
示例:设计一套管理软件,有一个Person类,他有ID、Name和Level属性。可以如下定义:
using System;
namespace HelloEnum
{
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Level = "Boss";
}
}
class Person
{
public int ID { get; set; }
public string Name { get; set; }
public string Level { get; set; }
}
}
person.Level = “Boss”,这样定义Level值很容易错写大小写或者写错,容易造成bug,使用枚举对Level 进行改造,如下:
using System;
namespace HelloEnum
{
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Level = Level.Employee;
Person boss = new Person();
boss.Level = Level.Boss;
Console.WriteLine(boss.Level>person.Level);//结果为True
Console.WriteLine((int)Level.Employee);//结果为0
Console.WriteLine((int)Level.Manager);//结果为1
Console.WriteLine((int)Level.Boss);//结果为2
Console.WriteLine((int)Level.BigBoss);//结果为3
}
}
enum Level
{
Employee,
Manager,
Boss,
BigBoss,
}
class Person
{
public int ID { get; set; }
public string Name { get; set; }
public Level Level { get; set; }
}
}
当用枚举定义Level时,在访问person的Level,只能在定义的属性中进行选择,此时默认Employee值为0,Manager值为1,Boss值为2,BigBoss值为3,也可以给他们进行赋值,如下:
enum Level
{
Employee=100,
Manager,//未赋值,值默认为101
Boss=200,
BigBoss,//未赋值,值默认为201
}
当Person增加有Skill技能,Skill中有Drive、Cook、Program、Teach四个值,当一个人都具有这4个技能时,应该用比特位式用法,如下:
using System;
namespace HelloEnum
{
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Level = Level.Employee;
person.Name = "Tim";
person.Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach;
//比特位式用法,‘|’为对位取‘或’。
Console.WriteLine(person.Skill);//结果为15
Console.WriteLine((person.Skill & Skill.Cook)>0);
//结果为True,说明person具有Cook技能。‘&’为对位取‘与’。
}
}
enum Level
{
Employee,
Manager,
Boss,
BigBoss,
}
enum Skill
{
Drive=1,//0001
Cook=2,//0010
Program=4,//0100
Teach=8,//1000
}
class Person
{
public int ID { get; set; }
public string Name { get; set; }
public Level Level { get; set; }
public Skill Skill { get; set; }
}
}
比特位用法未来使用广泛,如:读写文件时,控制文件的打开方式。串口参数的校验位、停止位。
结构体(struct)
基本定义
- 结构体类型是值类型,可装箱/拆箱
- 可实现接口,不能派生自类/结构体
- 不能有显示无参构造器
示例
值类型特点是,与值类型变量相关联的那块内存里存储的就是值类型的实例,而非像引用类型变量那样存储的是类型实例在堆内存中对应的地址,如下:
using System;
namespace HelloEnum
{
class Program
{
static void Main(string[] args)
{
Student student = new Student() { ID = 101, Name = "Tim" };
//student为局部变量,它是分配在Main函数的函数栈上,此变量所关联的内存直接存储的是Student类型的实例,
object obj = student;//装箱
Student student1 = (Student)obj;//拆箱
Console.WriteLine($"#{student1.ID} Name:{student1.Name}");//结果为:#101 Name:Tim
}
}
struct Student
{
public int ID { get; set; }
public string Name { get; set; }
}
}
一个值类型变量在赋值为另外一个值类型变量时,并不是引用,而是完整的copy,两个变量在内存中的变量是完全不同的,他们可以独立的变化。如下:
using System;
namespace HelloEnum
{
class Program
{
static void Main(string[] args)
{
Student student = new Student() { ID = 101, Name = "Tim" };
Student student1 = student;
student1.ID = 1001;
student1.Name = "Tom";
Console.WriteLine($"#{student.ID} Name:{student.Name}");//结果为:#101 Name:Tim
}
}
struct Student
{
public int ID { get; set; }
public string Name { get; set; }
}
}
结构体是可以实现接口的,如下:
using System;
namespace HelloEnum
{
class Program
{
static void Main(string[] args)
{
Student student = new Student() { ID = 101, Name = "Tim" };
student.Speak();//结果为:I'm #101 student:Tim
}
}
interface ISpeak
{
void Speak();
}
struct Student:ISpeak
{
public int ID { get; set; }
public string Name { get; set; }
public void Speak()
{
Console.WriteLine($"I'm #{this.ID} student:{this.Name}");
}
}
}
结构体不能有自己的基类或基结构体,且不能拥有显示的无参构造器,但可以显示显示有参构造器,如下:
struct Student
{
public int ID { get; set; }
public string Name { get; set; }
public Student(int id,string name)
{
this.ID = id;
this.Name = name;
}
}