1. 高级编程

1.1. 委托、Lambda表达式和事件

1.1.1. 委托

委托是安全封装方法的类型,类似于 C 和 C++ 中的函数指针。

与 C 函数指针不同的是,委托是面向对象的、类型安全的和可靠的。

说白了,委托是一个类,将方法作为实参传递,实际传递的是方法地址/引用。

先看这个例子:

/// <summary>
/// 定义委托类型
/// </summary>
/// <param name="array"></param>
/// <returns></returns>
delegate void GetNumberDelegate(int[] array);

class TestOperationNumber
{
    /// <summary>
    /// 显示运算结果
    /// </summary>
    /// <param name="dele">委托变量,符合委托定义的方法</param>
    public void Show(GetNumberDelegate dele)
    {
        int[] arr1 = { 1, 1, 2, 3, 5, 8 };
        dele(arr1);
    }

    public void GetMax(int[] array)
    {
        int max = array[0];
        foreach (int item in array)
        {
            if (max < item)
            {
                max = item;
            }
        }
        Console.WriteLine("比较得到最大值:" + max);
    }

    public void GetMin(int[] array)
    {
        int min = array[0];
        foreach (int item in array)
        {
            if (min > item)
            {
                min = item;
            }
        }
        Console.WriteLine("比较得到最小值:" + min);
    }

    public void GetSum(int[] array)
    {
        int sum = 0;
        foreach (int item in array)
        {
            sum += item;
        }
        Console.WriteLine("遍历求和结果:" + sum);
    }
}

class Program
{
    static void Main(string[] args)
    {
        TestOperationNumber opt = new TestOperationNumber();
        //opt.Show(opt.GetMax);
        //opt.Show(opt.GetMin);
        //opt.Show(opt.GetSum);

        GetNumberDelegate d1 = new GetNumberDelegate(opt.GetMax);
        d1 += opt.GetSum;
        opt.Show(d1);
    }
}
//委托调用的方法和委托的定义必须保持一致,如下面的几个示例
void Say(){}
delegate void DelegateTalk();

string Say(){}
delegate string DelegateTalk();

bool Say(int value){}
delegate bool DelegateTalk(int value);

方法作为参数进行传递

static void Main(string[] args)
{
    BackHome(BuyTicket);
    BackHome(Subway);

    Console.ReadKey();
}

/// <summary>
/// 无参,返回值为void的委托
/// </summary>
public delegate void DelegateBack();

static void BackHome(DelegateBack action)
{
    action();
}

static void BuyTicket()
{
    Console.WriteLine("买火车票");
}

static void Subway()
{
    Console.WriteLine("换乘地铁");
}

针对上例中的Main方法也可以修改为以下方式,以合并委托(多路广播委托)的方式实现

一个委托变量可以使用“+=”不断“挂接”多个方法,

也能使用“-=”动态地移除某个方法引用,还可以把多个委托变量所引用的方法合并起来。

static void Main(string[] args)
{
    DelegateBack action = new DelegateBack(BuyTicket);
    action += Subway;

    BackHome(action);

    Console.ReadKey();
}

1.1.2. Lambda表达式

Lambda 表达式是一种可用于创建 委托 或 表达式目录树 类型的 匿名函数 。

通过使用 lambda 表达式,可以写入可作为参数传递或作为函数调用值返回的本地函数。

Lambda 表达式对于编写 LINQ 查询表达式特别有用。

Lambda 表达式(C# 编程指南)

在 2.0 之前的 C# 版本中,声明委托的唯一方法是使用命名方法。 C# 2.0 引入了匿名方法,而在 C# 3.0 及更高版本中。

Lambda 表达式取代了匿名方法,作为编写内联代码的首选方式。

在C#2.0之前就有委托了,在2.0之后又引入了匿名方法,C#3.0之后,又引入了Lambda表达式。

他们三者之间的顺序是:委托->匿名表达式->Lambda表达式。

以下分别是三种对应不同的实现:

/// <summary>
/// 计算委托的类型
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
delegate int DelegateCalculate(int x, int y);

/// <summary>
/// 执行计算方法,但具体执行什么运算并不清楚
/// </summary>
/// <param name="func">委托传入的方法</param>
/// <param name="x">操作数1</param>
/// <param name="y">操作数2</param>
/// <returns></returns>
static int DoCalc(DelegateCalculate func, int x, int y)
{
    // 执行运算前的某些操作...
    return func.Invoke(x, y);
    // 执行运算后的某些操作...
}

/// <summary>
/// 加法
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
static int Add(int x, int y)
{
    return x + y;
}

static void Main(string[] args)
{
    /*
    委托
    加法运算,最简单的委托方式,先定义方法Add,再进行传入
    */
    int res1 = DoCalc(Add, 1, 2);

    /*
    匿名方法
    减法运算,使用匿名方法形式传入
    */
    int res2 = DoCalc(delegate (int x, int y)
    {
        return x - y;
    }, 1, 2);

    /*
    Lambda表达式
    乘法运算,【=>】左侧(x,y)为参数,【=>】右侧为代码块
    若要创建 Lambda 表达式,需要在 Lambda 运算符 => 左侧指定输入参数(如果有),然后在另一侧输入表达式或语句块。
    */
    int res3 = DoCalc((x, y) =>
    {
        return x * y;
    }, 2, 3);

    //上述乘法运算也可以简写为:
    int res4 = DoCalc((x, y) => x * y, 2, 3);
}

Lambda表达式"是一个特殊的匿名函数,是一种高效的类似于函数式编程的表达式,Lambda简化了开发中需要编写的代码量。

它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型,支持带有可绑定到委托或表达式树的输入参数的内联表达式。

所有Lambda表达式都使用Lambda运算符=>,该运算符读作"goes to"。

Lambda运算符的左边是输入参数(如果有),右边是表达式或语句块。

Lambda表达式x => x * x读作"x goes to x times x"。

上述示例也可以使用.NET预定义的委托Func<>进行替代,不需要新定义DelegateCalculate委托,如下:

/// <summary>
/// 计算委托的类型
/// 注释该委托,使用Func<>委托代替
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
//delegate int DelegateCalculate(int x, int y);

/// <summary>
/// 执行计算方法
/// Func<int,int,int>前两个int为参数,最后一个int为返回值类型
/// </summary>
/// <param name="fun">委托传入的方法 使用.NET预定义的委托</param>
/// <param name="x">操作数1</param>
/// <param name="y">操作数2</param>
/// <returns></returns>
static int DoCalc(Func<int, int, int> fun, int x, int y)
{
    // 执行运算前的某些操作...
    return func.Invoke(x, y);
    // 执行运算后的某些操作...
}

仅当 lambda 只有一个输入参数时,括号才是可选的;否则括号是必需的。 括号内的两个或更多输入参数使用逗号加以分隔:

(x, y) => x == y
x=> x*x

使用空括号指定零个输入参数:

() => SomeMethod()

1.1.3. 预定义的委托类型

泛型委托-Predicate

表示定义一组条件并确定指定对象是否符合这些条件的方法。

此委托由 Array 和 List 类的几种方法使用,用于在集合中搜索元素。

public delegate bool Predicate<T>(T obj);

类型参数介绍:

  • T: 要比较的对象的类型。
  • obj: 要按照由此委托表示的方法中定义的条件进行比较的对象。
  • 返回值:bool,如果 obj 符合由此委托表示的方法中定义的条件,则为 true;否则为 false。
static void Main()
{
    List<string> listStr = new List<string>() {
        "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" };
    string[] arrStr = listStr.ToArray();

    /*
    目标:筛选出长度小于等于3的元素
    */

    //1、传统方法-遍历
    foreach (var item in listStr)
    {
        if (item.Length <= 3)
        {
            Console.WriteLine(item);
        }
    }
    foreach (var item in arrStr)
    {
        if (item.Length <= 3)
        {
            Console.WriteLine(item);
        }
    }

    // public delegate bool Predicate<T>(T obj);
    //2、使用 Predicate 预先定义好方法的方式
    Predicate<string> pred1 = new Predicate<string>(GetFilter);
    List<string> list1 = listStr.FindAll(pred1);
    Console.WriteLine(string.Join(",", list1));


    //3、使用lambda表达式,一步筛选得出结论
    List<string> list2 = listStr.FindAll(t => t.Length <= 3);
    Console.WriteLine(string.Join(",", list2));
}

/// <summary>
/// 筛选元素
/// </summary>
/// <param name="val"></param>
/// <returns></returns>
static bool GetFilter(string val)
{
    return val.Length <= 3;
}

泛型委托-Action

Action泛型委托代表了一类方法:可以有0个到16个输入参数,输入参数的类型是不确定的,但不能有返回值。

//Action 等效
delegate void Delegate1();

//Action<int, string, bool, object> 等效
delegate void Delegate2(int num, string str, bool isa, object obj);

泛型委托-Func

为了弥补Action泛型委托,不能返回值的不足,.net提供了Func泛型委托。

相同的是它也是最多0到16个输入参数,参数类型由使用者确定,不同的是它规定要有一个返回值,返回值的类型也由使用者确定。

//Func<string> 等效
delegate string Delegate1();

//Func<string, bool> 等效
delegate bool Delegate2(string name);

//Func<string, bool, object, int> 等效
delegate int Delegate3(string str, bool isa, object obj);

1.1.4. 事件

事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些出现,如系统生成的通知。

应用程序需要在事件发生时响应事件。例如,中断。事件是用于进程间通信。

事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。

包含事件的类用于发布事件。这被称为 发布器(publisher) 类

其他接受该事件的类被称为 订阅器(subscriber) 类

事件使用 发布-订阅(publisher-subscriber) 模型

发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。

订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。

使用Action<>类型定义事件,添加事件监听并触发。

class Student
{
    /// <summary>
    /// 打招呼事件,等价于委托 delegate void DelegatePrint(string xxx); 
    /// 仅仅只是定义了可以进行打招呼,但具体怎么打招呼,由外部进行确定
    /// </summary>
    public Action<string> GreetEvent;
}

class Program
{
    static void Main()
    {
        Student s1 = new Student();
        s1.GreetEvent = new Action<string>(SayHi);//添加注册事件
        s1.GreetEvent("jack");

        Console.WriteLine("===========================");
        Student s2 = new Student();
        s2.GreetEvent = new Action<string>(SayHi);//添加注册事件
        s2.GreetEvent += SayNice;//添加注册事件
        s2.GreetEvent("lucy");
    }

    static void SayHi(string name)
    {
        Console.WriteLine($"hi,{name}");
    }

    static void SayNice(string name)
    {
        Console.WriteLine($"nice to meet you,{name}");
    }
}

上面的示例中并未对【GreetEvent】添加event关键字,故可以直接通过【对象.事件名】进行触发。

普通委托添加【event】关键字后,只能由成员方法进行调用触发。

class Student
{
    /// <summary>
    /// 打招呼事件,等价于委托 delegate void DelegatePrint(string xxx); 
    /// 仅仅只是定义了可以进行打招呼,但具体怎么打招呼,由外部进行确定
    /// 使用event关键字为了避免直接在对象上进行委托实例的调用,如【stu1.GreetEvent("xxx");】
    /// 使用event关键,无法在类外部通过对象名进行重新赋值,只允许进行添加和移除操作
    /// </summary>
    public event Action<string> GreetEvent;

    /// <summary>
    /// 开始打招呼
    /// </summary>
    /// <param name="otherName"></param>
    public void StartGreet(string otherName)
    {
        if (GreetEvent != null)
        {
            GreetEvent.Invoke(otherName);
        }
    }
}

class Program
{
    static void Main()
    {
        Student s1 = new Student();
        //s1.GreetEvent = new Action<string>(SayHi);
        s1.GreetEvent += SayHi;
        //s1.GreetEvent("jack");
        s1.StartGreet("jack");

        Console.WriteLine("===========================");
        Student s2 = new Student();
        //s2.GreetEvent = new Action<string>(SayHi);
        s2.GreetEvent += SayHi;
        s2.GreetEvent += SayNice;
        //s2.GreetEvent("jack");
        s2.StartGreet("lucy");
    }

    static void SayHi(string name)
    {
        Console.WriteLine($"hi,{name}");
    }

    static void SayNice(string name)
    {
        Console.WriteLine($"nice to meet you,{name}");
    }
}

以上案例,【event】这样的设计规范,【event】只允许在类的成员方法中进行Invoke,为了明确事件发送对象。

【event】只允许通过【对象.event】添加/移除事件订阅,不允许重新实例化。

针对上面案例进一步修改,对某一对象添加事件订阅和移除事件订阅。

class Student
{
    /// <summary>
    /// 打招呼事件,等价于委托 delegate void DelegatePrint(string xxx); 
    /// 仅仅只是定义了可以进行打招呼,但具体怎么打招呼,由外部进行确定
    /// 使用event关键字为了避免直接在对象上进行委托实例的调用,如【stu1.GreetEvent("xxx");】
    /// 使用event关键,无法在类外部通过对象名进行重新赋值,只允许进行添加和移除操作
    /// </summary>
    public event Action<string> GreetEvent;

    /// <summary>
    /// 开始打招呼
    /// </summary>
    /// <param name="otherName"></param>
    public void StartGreet(string otherName)
    {
        if (GreetEvent != null)
        {
            GreetEvent.Invoke(otherName);
        }
    }
}

class Program
{
    static void Main()
    {
        Student s1 = new Student();
        s1.GreetEvent += SayHi;
        s1.StartGreet("jack");

        Console.WriteLine("===========================");
        Student s2 = new Student();
        s2.GreetEvent += SayHi;
        s2.GreetEvent += SayNice;

        // 以lambda表达式方式添加事件注册,仅一次性使用,无法进行注册移除
        s2.GreetEvent += (s) =>
        {
            Console.WriteLine($"你好,{s}");
        };
        s2.GreetEvent -= SayNice; //移除事件注册
        s2.StartGreet("lucy");
    }

    static void SayHi(string name)
    {
        Console.WriteLine($"hi,{name}");
    }

    static void SayNice(string name)
    {
        Console.WriteLine($"nice to meet you,{name}");
    }
}

1.2. Enumerable支持标准查询的操作符

1.2.1. 匿名类型和隐式类型

匿名类型是由编译器声明的数据类型,当编译器看到匿名类型时,会执行一些后台操作,生成这个类,并允许像已经显式声明过它那样使用。

var book1 = new { Title = "黄金时代", Auth = "王小波", Price = 29 };
var book2 = new { Title = book1.Title };

匿名类型纯粹是一个C#语言特性,不是"运行时"中的一种新类型。

需要注意的是,除非赋给变量的类型能一眼看出,否则应该只有在声明匿名类型(具体类型只有在编译时才能确定)时,才使用隐式类型的变量。

不要不分青红皂白地使用隐式类型(var)的变量,这里的var和JavaScript中的var是不一样的概念。

匿名类型的安全性和不可变性

var book1 = new { Title = "黄金时代", Auth = "王小波", Price = 29 };
var book2 = new { Title = book1.Title };

// 隐式转换类型
//book1 = book2;

// 无法为属性赋值,它是只读的
//book1.Title = "青铜时代";

匿名类型之间不兼容,并且匿名类型是不可变的,所以匿名类型一经实例化,就无法修改其属性值。

1.2.2. IEnumerable

集合实质上就是一个类,实现了IEnumerable<T>接口。

这个接口非常重要,要想支持对集合执行的遍历操作,最起码要求就是实现IEnumerable接口。

C#编译器不要求一定要实现IEnumerable/IEnumerable才能用foreach对一个数据类型进行迭代。

相反,编译器采用一个称为"Duck typing"的概念;也就是查找一个GetEnumerator()方法,这个方法返回包含Current属性和MoveNe×t()方法的一个类型。

Duck typing按名称查找方法,而不是依赖接口或显式方法调用。

如果找不到可枚举模式的恰当实现,编译器就检查集合是否实现了接口。

foreach循环内不要修改集合!!!

1.2.3. 标准查询操作符

IEnumerable<T>上的每个方法都是一个标准查询操作符,用于为所操作的集合提供查询功能。

以下案例均基于Product和Provider类,代码如下:

/// <summary>
/// 商品类
/// </summary>
public class Product
{
    public Product(long prodId, string name, string productLocation, double price)
    {
        ProdId = prodId;
        Name = name;
        ProductLocation = productLocation;
        Price = price;
    }

    public long ProdId { get; set; }
    public string Name { get; set; }
    public string ProductLocation { get; set; }
    public double Price { get; set; }
    /// <summary>
    /// 重写ToString方法,方便打印输出
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"{Name}(产地:{ProductLocation},价格:{Price})";
    }
}

/// <summary>
/// 供应商
/// </summary>
public class Provider
{
    public Provider(long providerId, string name, string city, List<long> prodList)
    {
        ProviderId = providerId;
        Name = name;
        City = city;
        ProdList = prodList;
    }

    public long ProviderId { get; set; }
    public string Name { get; set; }
    public string City { get; set; }
    /// <summary>
    /// 可供应商品
    /// </summary>
    public List<long> ProdList { get; set; }
    public override string ToString()
    {
        return $"{City}-{Name}";
    }
}


public static class TestData
{
    /// <summary>
    ///  商品数组,测试数据
    /// </summary>
    public static readonly Product[] ProductsArray = new Product[] {
        new Product(1, "黑人牙膏", "芜湖", 12),
        new Product(2, "佳洁士牙膏", "芜湖", 4.5),
        new Product(3, "黑人牙刷", "合肥", 5.5),
        new Product(4, "舒克牙刷", "合肥", 9.9),
        new Product(5, "心相印抽纸", "合肥", 10.9),
        new Product(6, "清风抽纸", "合肥", 12.9),
    };

    /// <summary>
    /// 供应商数组,测试数据
    /// </summary>
    public static readonly Provider[] ProvidersArray = new Provider[] {
        new Provider(101, "苏宁小店", "芜湖市弋江区", new List<long>() { 1, 2 }),
        new Provider(102, "苏宁小店", "芜湖市镜湖区", new List<long>() { 3 }),
        new Provider(103, "呆萝卜", "芜湖市镜湖区", new List<long>() { 3, 4 }),
        new Provider(104, "呆萝卜", "芜湖市鸠江区", new List<long>() { 4, 5 }),
        new Provider(105, "京东小店", "合肥市庐阳区", new List<long>() { 4, 5 }),
        new Provider(106, "京东小店", "合肥市蜀山区", new List<long>() { 6 }),
    };
}

public class Program
{
    static void Main(string[] args)
    {
        IEnumerable<Product> products = TestData.ProductsArray;
        Print(products);

        IEnumerable<Provider> providers = TestData.ProvidersArray;
        Print(providers);
    }

    /// <summary>
    /// 泛型方法,集合对象
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="items"></param>
    static void Print<T>(IEnumerable<T> items)
    {
        foreach (T item in items)
        {
            Console.WriteLine(item.ToString());
        }
    }
}

Where筛选

从集合中筛选出数据,需要提供筛选器方法返回true或false以指明特定的元素是否应该被包含进来。

获取一个实参并返回一个布尔值的lambda表达式称为谓词

集合的Where()方法依据谓词来确定筛选条件,下面案例是筛选所有价格大于10的商品

// 筛选所有价格大于10的商品
var priceThan10 = TestData.ProductsArray.Where(t => t.Price > 10);
Console.WriteLine("****************筛选所有价格大于10的商品****************");
Print(priceThan10);

Select投射

由于IEnumerable<T>.Where()输出的是一个新的IEnumerable<T>集合,所以完全可以在这个集合的基础上再调用另一个标准查询操作符。

例如,从原始集合中筛选好数据后,可以接着对这些数据进行转换,如下所示:

var priceThan10 = TestData.ProductsArray.Where(t => t.Price > 10);
// 生成新的字符串结合
IEnumerable<string> prodInfo = priceThan10.Select(t => $"{t.Name}(价格:{t.Price})");
Console.WriteLine("****************筛选所有价格大于10的商品,新投影的列****************");
Print(prodInfo);

匿名类型,在创建IEnumerable<T>集合时,T可以是匿名类型,如下使用Select()投射匿名类型

// 获取当前路径下所有文件
IEnumerable<string> fileList = Directory.EnumerateFiles(AppDomain.CurrentDomain.BaseDirectory);
// 重新投影,选择文件名称和文件大小
var items = fileList.Select(t =>
{
    FileInfo info = new FileInfo(t);
    return new { FileName = info.Name, Size = $"{info.Length / 1024 }Kb" };
});
Console.WriteLine("****************获取当前debug目录下所有文件,重新投影名称和大小****************");
Print(items);

在为匿名类型生成的ToString()方法中,会自动添加用于显示属性名称及其值的代码。

使用Select()进行“投射",这是非常强大的一个功能。

上一节讲述了如何使用Where()标准查询操作符在“垂直"方向上筛选集合工(减少集合中项的数量)。

现在,使用Select()标准查询操作符,还可以在“水平"方向上减小集合的规模(减少列的数量)或者对数据进行彻底的转换。

综合运用Where()Select(),可以获得原始集合的一个子集,从而满足当前算法的要求。

这两个方法各自提供了一个功能强大的、对集合进行操纵的API。

使用Count()对元素进行计数

Console.WriteLine($"Products Count:{TestData.ProductsArray.Count()}");
Console.WriteLine($@"Product's price than ¥10 :{
    TestData.ProductsArray.Count(t => t.Price > 10)}");

虽然Count()语句看起来简单,但IEnumerable没有改变,所以真正执行的代码仍然会遍历集合中的所有项。

如果集合直接提供一个Count属性,就应该首选属性,而不要用LINQ的Count()方法(这是一个许多人都没有意识到的差异)。

幸好,ICollection<T>包含了Count属性,所以如果集合支持ICollection<T>,那么在它上面调用Count()方法,会对集合进行转型,并直接调用Count。

// 常用的 List 实现了`ICollection<T>`接口,即针对List可以直接使用Count属性
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable{}

然而,如果不支持ICollection<T>,Enumerable.Count()就会枚举集合中的所有项,而不是调用内建的Count机制。

如果计数的目的只是为了看这个计数是否大于0,那么首选的做法是使用Any()操作符。

Any()只尝试遍历集合中的一个项,如果成功就返回true,而不会遍历整个序列。如下例所示:

// 效率低
if(TestData.ProductsArray.Count() > 0) {...}

// 建议采用
if(TestData.ProductsArray.Any()) {...}

OrderBy和ThenBy排序

基于上面的案例,使用Price作为排序的键,返回的仍然是IEnumerable集合类型

var orderPrice = TestData.ProductsArray.OrderBy(t => t.Price);
Console.WriteLine("****************按price价格升序排列****************");
Print(orderPrice);

ThenBy用于多个列的分组,先按照产品产地排序,再按照价格进一步排序,如下所示:

var orderPrice = TestData.ProductsArray.OrderBy(t => t.ProductLocation)
    .ThenBy(t => t.Price);
Console.WriteLine("****************先按产地排序,再按照price价格排序****************");
Print(orderPrice);

GroupBy分组

var locationGroup = TestData.ProductsArray.GroupBy(t => t.ProductLocation);
Console.WriteLine("****************按照产地分组,再遍历分组明细****************");
// 遍历得到的分组
foreach (var item in locationGroup)
{
    Console.WriteLine($"key:{item.Key},Count:{item.Count()}");
    foreach (var prod in item)
    {
        Console.WriteLine(prod);
    }
}

注意,GroupBy()返回的是IEnumerable<IGrouping<TKey, TSource>>类型的数据项,

如果需要对多个列进行分组,参考如下代码:

// 按照多个列进行分组,产地和取整价格
var locationGroup = TestData.ProductsArray.GroupBy(prod => new
{
    Location = prod.ProductLocation,
    PriceFloor = Math.Floor(prod.Price)
});
Console.WriteLine("****************按照产地和取整价格进行分组****************");
// 遍历得到的分组
foreach (var item in locationGroup)
{
    Console.WriteLine($"分组Key:{item.Key},Count:{item.Count()}");
    foreach (var prod in item)
    {
        Console.WriteLine(prod);
    }
}

针对上述案例的petsList修改,可以通过第二个传参指定返回匿名类型

// 按照指定key进行group by,并返回新的匿名类型
var locationGroup = TestData.ProductsArray.GroupBy(prod => prod.ProductLocation,
    prod =>
    {
        // 投影新的查询对象
        return new { Name = $"{prod.Name}-{prod.ProductLocation}", PriceFloor = Math.Floor(prod.Price) };
    });
Console.WriteLine("****************按照产地和取整价格进行分组****************");
// 遍历得到的分组
foreach (var item in locationGroup)
{
    Console.WriteLine($"分组Key:{item.Key},Count:{item.Count()}");
    foreach (var prod in item)
    {
        Console.WriteLine(prod);
    }
}

在分组时也可以基于分组产生新的分组信息,如下:

/*
生成的不再是 IEnumerable<IGrouping<string, Product>> 类型,而是IEnumerable<>集合
第1个参数是按照哪个列进行分组,
第2个参数是每个分组里的数据处理,
第3个参数是分组后每组数据处理,产生新的匿名类型,(key,keyList)=>{ return new {}}
*/
var query = TestData.ProductsArray.GroupBy(prod => prod.ProductLocation,
    prod => prod,
    (location, groupProducts) =>
    {
        return new
        {
            Location = location,
            Size = groupProducts.Count(),
            MaxPrice = groupProducts.Max(t => t.Price),
            MinPrice = groupProducts.Min(t => t.Price),
        };
    });
Console.WriteLine("****************按照城市分组,并查找分组内最高/最低价格****************");
foreach (var item in query)
{
    Console.WriteLine(item);
}

1.3. 扩展方法

如果想给一个类型增加行为,一定要通过继承的方式实现吗?不一定的!

比如我们想要给String类添加打印输出到控制台的方法,可以通过如下方式实现:

static class Program
{
    /// <summary>
    /// 扩展方法,针对string类型增加打印输出到控制台的方法
    /// 注意,扩展方法必须放在非泛型静态类中,方法也需要声明为静态方法
    /// </summary>
    /// <param name="source"></param>
    public static void PrintConsole(this string source)
    {
        Console.WriteLine(source);
    }

    static void Main(string[] args)
    {
        string name = "不识美妻刘强东";
        //扩展方法的调用
        name.PrintConsole();
    }
}

1.4. 反射

在运行期间处理和检测代码,反射指程序可以访问、检测和修改它本身状态或行为的一种能力。

程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。

使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。

1.4.1. 反射(Reflection)

加载同程序集

public class Student
{
    public Student(string name)
    {
        Name = name;
    }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Course { get; set; }
    public void Say(string otherName)
    {
        Console.WriteLine("hi {0},how do u do,i'm {1}", otherName, Name);
    }
}

public class Program
{
    static void Main(string[] args)
    {
        //以下的type1、type2、type3的是相同的
        Student stu1 = new Student("");
        Type type1 = stu1.GetType();

        Type type2 = typeof(Student);

        // Reflection 为命名空间名称
        Type type3 = Type.GetType("Reflection.Student");

        /*
        加载程序集中类型,并打印显示到控制台
        */
        Assembly a = Assembly.LoadFrom("Reflection.exe");
        Type[] types = a.GetTypes();
        foreach (Type item in types)
        {
            Console.WriteLine("类型名称为:" + item.Name);
        }

        /*
        反射查看类内的成员
        */
        Type type = typeof(Student);
        //获取类内的所有公开字段
        FieldInfo[] fieldInfos = type.GetFields();
        //获取类内所有公开属性
        PropertyInfo[] propInfos = type.GetProperties();
        //获取类内所有公开方法
        MethodInfo[] methodInfos = type.GetMethods();
        //获取类内所有公开成员,包含了字段、属性、方法等
        MemberInfo[] memInfos = type.GetMembers();
        foreach (MemberInfo item in memInfos)
        {
            Console.WriteLine("MemberType:{0},Name:{1}", item.MemberType, item.Name);
        }

        /*
        通过反射构造对象,调用方法
        */
        //使用指定类型的默认构造函数来创建该类型的实例,实例化一个对象
        object obj = Activator.CreateInstance(type, new object[] { "宋小宝" });
        //获取指定的方法 
        MethodInfo sayMethod = type.GetMethod("Say");
        //执行Student类中的Say方法
        var result = sayMethod.Invoke(obj, new object[] { "王富贵" });
    }

}

看了上面的代码,也许会有疑问,既然在开发时就能够写好代码,干嘛还放到运行期去做,不光繁琐,而且效率也受影响。

很多设计模式是基于反射实现的,设计模式的好处是复用解决方案,可靠性高等。如何取舍是一个见仁见智的问题。。。

加载不同程序集

通过加载其他程序集中的类,进行实例化对象和调用方法:

新建类库项目,默认命名【ClassLibrary1】,然后创建【Teacher】类,结构内容如下:

namespace ClassLibrary1
{
    public class Teacher
    {
        public string Name { get; set; }
        public Teacher(string name) { Name = name; }
        public void Say(string msg)
        {
            Console.WriteLine($"hello {msg},i'm {Name}");
        }
    }
}

新增控制台应用程序项目,编译上一步中的类库项目,并拷贝【ClassLibrary1.dll】文件至控制台应用程序的【debug】目录

在控制台应用程序的【Main】方法中进行加载程序集并调用,代码如下:

// 装载程序集 ClassLibrary1.dll,在当前目录中
Assembly ass = Assembly.LoadFrom("ClassLibrary1.dll");

// 获取该程序集中定义的 Teacher类,注意要写全名FullName,如 ClassLibrary1.Teacher
Type t = ass.GetType("ClassLibrary1.Teacher");

// 需要考虑找不到该类型的情况
if (null != t)
{
    object jack = Activator.CreateInstance(t, "宋小宝");
    MethodInfo method = t.GetMethod("Say");
    method.Invoke(jack, new object[] { "这是通过assembly反射进行调用的方法" });
}

1.4.2. 自定义特性(Attribute)

特性是什么

Attribute 是一种可由用户自由定义的修饰符(Modifier),可以用来修饰各种需要被修饰的目标。

简单的说,Attribute就是一种“附着物” —— 就像牡蛎吸附在船底或礁石上一样。

这些附着物的作用是为它们的附着体追加上一些额外的信息(这些信息就保存在附着物的体内)

比如这个属性对应数据库中哪个字段,这个类对应数据库中哪张表等等。

作用

特性Attribute 的作用是添加元数据。

元数据可以被工具支持,比如:编译器用元数据来辅助编译,调试器用元数据来调试程序。

Attribute 与注释的区别

  • 注释是对程序源代码的一种说明,主要目的是给人看的,在程序被编译的时候会被编译器所丢弃,因此,它丝毫不会影响到程序的执行。
  • Attribute是程序代码的一部分,不但不会被编译器丢弃,而且还会被编译器编译进程序集(Assembly)的元数据(Metadata)里,在程序运行的时候,你随时可以从元数据里提取出这些附加信息来决策程序的运行。

使用

自定义特性的定义:

public sealed class FieldChNameAttribute : Attribute
{
    public string ChName { get; set; }
    public FieldChNameAttribute() { }
    public FieldChNameAttribute(string chName)
    {
        ChName = chName;
    }
}

如何使用自定义特性:

[FieldChName(ChName = "学生实体")]
public class Student
{
    [FieldChName(ChName = "姓名")]
    public string Name { get; set; }

    [FieldChName(ChName = "年龄")]
    public int Age { get; set; }
    public string Course { get; set; }
}

检查类、属性是否有标记特性,以及获取特性的属性值:

static void Main(string[] args)
{
    Type type = typeof(Student);

    // 获取Student类上自定义特性
    FieldChNameAttribute attr = type.GetCustomAttribute(typeof(FieldChNameAttribute), false) as FieldChNameAttribute;

    //该类有FieldChaNameAttribute自定义特性,则获取设置的属性值ChName
    if (null != attr)
    {
        // 学生类上的标签注解
        Console.WriteLine(attr.ChName);
    }

    // 获取类型的所有公开属性
    PropertyInfo[] props = type.GetProperties();

    // 遍历公开属性
    foreach (PropertyInfo pp in props)
    {
        // 获取属性上的自定义特性
        FieldChNameAttribute ppAttr = pp.GetCustomAttribute(typeof(FieldChNameAttribute), false) as FieldChNameAttribute;
        // 如属性有自定义特性,则获取设置的属性值ChName
        if (null != ppAttr)
        {
            Console.WriteLine(ppAttr.ChName);
        }
    }
}

1.5. 自定义集合(了解)

在System.Collections 命名空间下,常用的集合类中,有两个类不属于集合,而是作为自定义集合类的基类。

  • CollectionBase:为强类型集合提供abstract 基类
  • DictionaryBase:为键/值对的强类型集合提供abstract基类。

如果我们对自定义集合有更多要求的话,比如:

  • 能够通过索引号去访问集合中的某个元素,则需要定义集合的索引器
  • 能够通过foreach循环遍历每一个元素,则需要定义集合的迭代器
class Program
{
    static void Main(string[] args)
    {
        StudentCollection stuCollection = new StudentCollection();
        stuCollection.Add(new Student("jack"));
        stuCollection.Add(new Student("lucy"));

        //使用迭代器,因为CollectionBase实现了IEnumerable接口,所以可以直接使用foreach
        foreach (Student item in stuCollection)
        {
            item.SayHi();
        }

        //使用索引器进行方法调用
        stuCollection[1].SayHi();
    }
}

/// <summary>
/// 自定义CollectionBase集合
/// </summary>
public class StudentCollection : CollectionBase
{
    /// <summary>
    /// 重写父类中的Add方法,因为父类Add为私有方法,元数据中不可见
    /// CollectionBase源码中可见父类中实现了Add方法
    /// https://referencesource.microsoft.com/#mscorlib/system/collections/collectionbase.cs
    /// </summary>
    /// <param name="stu"></param>
    /// <returns></returns>
    public int Add(Student stu)
    {
        return List.Add(stu);
    }

    /// <summary>
    /// Remove方法同上Add方法,都是私有实现
    /// </summary>
    /// <param name="stu"></param>
    public void Remove(Student stu)
    {
        List.Remove(stu);
    }

    /// <summary>
    /// 父类中为普通方法,不可重写,只能使用new进行隐藏
    /// </summary>
    /// <param name="index"></param>
    public new void RemoveAt(int index)
    {
        List.RemoveAt(index);
    }

    /// <summary>
    /// 索引器
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public Student this[int index]
    {
        get { return List[index] as Student; }
        set { List[index] = value; }
    }
}

public class Student
{
    public Student(string name) { Name = name; }
    public string Name { get; set; }
    public void SayHi() { Console.WriteLine($"hello i'm {Name}"); }
}

关于迭代,foreach遍历是C#常见的功能,C#使用yield关键字让自定义集合实现foreach遍历的方法

一般来说当我们创建自定义集合的时候为了让其能支持foreach遍历,就只能让其实现IEnumerable接口(可能还要实现IEnumerator接口)

但是我们也可以通过使用yield关键字构建的迭代器方法来实现foreach的遍历,且自定义的集合不用实现IEnumerable接口

class Program
{
    static void Main(string[] args)
    {
        StudentList sts = new StudentList();
        foreach (Student item in sts)
        {
            item.SayHi();
        }
    }
}

public class StudentList
{
    private Student[] arr = new Student[3];
    public StudentList()
    {
        arr[0] = new Student("张三");
        arr[1] = new Student("李四");
        arr[2] = new Student("王富贵");
    }

    public IEnumerator GetEnumerator()
    {
        foreach (Student item in arr)
        {
            // yield return 作用就是返回集合的一个元素,并移动到下一个元素上
            yield return item;
        }
    }
}

public class Student
{
    public Student(string name) { Name = name; }
    public string Name { get; set; }
    public void SayHi() { Console.WriteLine($"hello i'm {Name}"); }
}

注意:虽然不用实现IEnumerable接口 ,但是迭代器的方法必须命名为GetEnumerator(),返回值也必须是IEnumerator类型。


参考引用:

C# 中自定义Attribute值的获取与优化

关于C# 中的Attribute 特性

FlagsAttribute 类

C#枚举中的位运算权限分配浅谈

results matching ""

    No results matching ""