您当前的位置: 首页 >  .net

寒冰屋

暂无认证

  • 1浏览

    0关注

    2286博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

.net core精彩实例分享 -- 泛型和集合

寒冰屋 发布时间:2021-05-10 22:18:06 ,浏览量:1

文章目录
  • 介绍
  • 具体案例
    • 限制泛型参数只能使用值类型
    • 泛型参数的输入和输出
    • 将抽象类作为类型约束
    • 使用Span提升处理字符串的性能
    • 多个Task同时操作ConcurrenBag集合
    • 跨线程访问BlockingCollection集合
  • 总结

介绍

随着.net core越来越流行,对.net core 基础知识的了解,实际应用等相关的知识也应该有所了解。所以就有了这篇文章,案例都是来自阅读的书籍,或者实际工作中感觉比较有用的应用。分享亦总结。

本文主要介绍 .net core 相关的泛型和集合案例。

具体案例 限制泛型参数只能使用值类型

【导语】

因为泛型参数可以是任意类型,所以如果编译器仅仅将参数假定位 Object 类型(即按照 Object 类的成员进行访问),那么代码在访问某些特定类型时就会引发编译器错误。

为了避免引发编译错误,有时候需要对泛型参数进行限制,也就是泛型约束。常用的泛型约束如下。

(1)class:限制泛型参数的类型必须是类(引用类型)。

(2)struct:限制泛型参数的类型必须是结构(值类型)。

(3)new():要求类型必须包含公共的、无参数的构造函数。当使用多个约束时,new() 必须放到最后。

(4)unmanaged:要求类型是非托管类型。此于数不常用。

(5)接口或者基类:要求类型必须实现指定接口,或者必须从指定类型派生。

泛型约束可以组合使用,例如下列泛型类要求类型参数是引用类型(class),而且要求实现 IService 接口。

public class  `Main` Item where U:calss,IService
{
    ...
}

泛型约束的使用方法是在声明类型或类型成员之后应用 where 关键字,然后才是类型参数的约束条件。如果由多个泛型参数,也可以用多个 where 关键字,格式如下。

public class SomeOne
    where T: class, new()
    where S: ITask
{

}

【操作流程】

步骤1:新建控制台应用程序项目。

步骤2:声明一个类,命名位 Test,带由一个泛型参数 T。

class Test
{
    ...
}

步骤3:对泛型参数 T 进行约束,限制只能是值类型,即类型为结构。

class Test where T : struct
{
    ...
}

步骤4:在类中声明一个 Start 方法,并接受一个 T 类型参数。

public void Start(T x)
{
    string CheckType(Type t) => t.IsValueType ? "是" : "不是";
    Type type = x.GetType();
    Console.WriteLine($"{type.Name} {CheckType(type)}值类型。");
}

CheckType 是一个内联方法,它的格式与方法类似,但是不需要指定方法修饰符,一般用于实现简单的逻辑处理。如本例中的 CheckType,只是用于判断类型 T 是否为值类型,如果是返回字符串“是”,否则返回字符串“不是”。

步骤5:回到 Main 方法,用定义好的泛型类声明一个变量,然后创建一个新实例。随后调用 Start 方法,此时类型参数 T 为 int 类型。

Test tv = new Test();
tv.Start(100);

步骤6:类似的,在声明一个 Test 类的变量实例化,然后调用 Start 方法,此时类型参数 T 为 byte 类型。

Test tq = new Test();
tq.Start(152);

步骤7:运行应用程序项目,结果如下。

在这里插入图片描述

泛型参数的输入和输出

【导语】

如果泛型参数不带任何修饰符,那么在分配对象实例时,类型参数只能是固定的类型。例如以下形式。

Class x = new Class();

假设 B 类从 A 类派生,则分配以下对象实例时会报错。

Class x = new Class();

因为泛型参数为使用任何修饰符,使用参数类型是固定的。声明变量时使用的是 A 类型,而分配对象实例时使用的是 B 类型,前后不一致,会出现编译错误。

要使泛型中的类型参数成为变体,一般可以使用两个修饰符————inout。带 in 修饰符的是“输入类型”,此类型参数一般用于委托或方法的输入参数,属于逆变。带 out 修饰符的是“输出参数”,一般用于委托或方法的返回值,属于协变。

泛型的输入/输出类型参数只能用于委托和接口两种数据类型,不能用于类和结构,而且作为类型参数的类型不能是值类型。

【操作流程】

步骤1:新建控制台应用程序项目。

步骤2:声明两个类,稍后用于测试

public class Ball { }
public class FootBall : Ball { }

FootBall 类从 Ball 类派生。按照隐式转换的要求,FootBall 类的实例可以赋值给使用 Ball 类型声明的变量。

步骤3:声明两个带可变类型参数的泛型接口。

public interface ITest1 { }
public interface ITest2 { }

ITest1 接口中,T 类型参数使用了 in 修饰符,表示它是一个输入类型,即逆变。在 ITest2 接口中,T 类型参数使用 out 修饰符,表示该类型将作为输出擦拭你和(返回值),即协变。

步骤4:声明两个类,分别实现 ITest1ITest2 接口。

public class Test1 : ITest1 { }
public class Test2 : ITest2 { }

步骤5:在 Main 方法中用 ITest1 接口声明变量,类型参数为 FootBall 类,然后用 Test1 类的新实例进行赋值,类型参数为 Ball 类。

ITest1 t1 = new Test1();

上述情况属于逆变。t1 变量的泛型参数只能接受 FootBall 类型,而它所引用的实例的泛型参数则可以接受 BallFootBall 两个类型,可以看到,赋值之后实例能分配的兼容性变小了,当通过 t1 变量调用相关成员时,只能使用 FootBall 类。

步骤6:用 ITest2 接口声明变量,并指定泛型参数为 Ball 类,使用 Test2 实例赋值的泛型参数为 FootBall 类。

ITest2 t2 = new Test2();

变量 t2 能接收 BallFootBall 两个类型的返回值,而它所引用的实例只能返回 FootBall 类型的对象。当使用 t2 变量调用相关成员时,由于ITest2的分配兼容性变大,能够顺利引用Test2实例所返回的对象,此情况属于协变。

将抽象类作为类型约束

【导语】

将泛型参数约束为抽象类或接口,可以有效规范对类型实例的访问,中在实际开发中比较实用。如果不给类型参数添加约束,那么在默认情况下编译器就会以 Object 类的成员进行规范,这个会带来诸多不便。

然而如果是应用抽象类或者接口来约束类型参数,那么在访问参数实例时就很方便了。例如声明一个接口,名为 IService,接口中声明两个 方法————OpenClose。在泛型参数中添加约束,要求类型必须实现 IService 接口。在这种情况下,访问泛型参数实例的代码不需要关心有多少个类实现了 IService 接口,因为不管是哪个类,只要它实现了该接口,必然会包含 OpenClose 这两个方法,如此一来,代码只需要带调用方法。

【操作流程】

步骤1:新建控制台应用程序项目。

步骤2:为了便于稍后测试,先声明一个抽象类 Animal,它表示所有动物,同时包含一个抽象的 CheckIn 方法。

/// 
/// 公共基类
/// 
public abstract class Animal
{
    public abstract void CheckIn();
}

所有从 Animal 类派生的类都必须实现 CheckIn 方法。

步骤3:声明 4 个新类,都实现 Animal 抽象类。

/// 
/// 猫鼬
/// 
public class Meerkat : Animal
{
    public override void CheckIn()
    {
        Console.WriteLine("这是猫鼬。\n");
    }
}
/// 
/// 狐狸
/// 
public class Fox : Animal
{
    public override void CheckIn()
    {
        Console.WriteLine("这是狐狸。\n");
    }
}
/// 
/// 鸡
/// 
public class Chicken : Animal
{
    public override void CheckIn()
    {
        Console.WriteLine("这是鸡。\n");
    }
}
/// 
/// 鹌鹑
/// 
public class Quail : Animal
{
    public override void CheckIn()
    {
        Console.WriteLine("这是鹌鹑。\n");
    }
}

步骤4:声明一个泛型接口,将类型参数标注为输入参数,这样可以在后续使用中支持传递不同派生程度的类。

public interface ITest
{
    void DoWork(T pr);
}

步骤5:声明泛型类,并是实现上面的泛型接口,将类型参数约束为必须是从 Animal 派生的类。

public class TestAnl : ITest
            where T : Animal
{
    public void DoWork(T pr)
    {
        // CheckIn 方法是在抽象类中定义的
        // 所有实现该抽象类的类型都能访问
        pr.CheckIn();
    }
}

步骤6:使用 ITest 接口声明一个变量,然后使用 TestAnl 类的新实例对其初始化,类型参数使用 Animal 类,这样做能够增加调用 DoWork 方法的灵活性,能够向该方法传递各种 Animal 类的子类实例。

ITest t = new TestAnl();

步骤7:调用 DoWork 方法,依次将 Animal 类的 4 个派生类实例传递进去。

t.DoWork(new Fox());
t.DoWork(new Meerkat());
t.DoWork(new Quail());
t.DoWork(new Chicken());

步骤8:运行应用程序项目,结果如下。

在这里插入图片描述

使用Span提升处理字符串的性能

【导语】

.NET 中许多类型都是在托管堆中分配内存的,在对连续内存卡进行操作时,某些数据类型会比较花时间,其中最典型的是字符串。在处理字符串的过程中会不断创建新的实例,这些过程必将占用一定的时间。

.NET Core 类库提供了一种特殊的结构————Span,它可以用于操作各种连续分布的内存数据。可以通过以下来源初始化 Span

  • 常见的托管数组。
  • 栈内存中的对象。
  • 本地内存指针。

此外,Span 支持 GC 功能,不需要显示释放内存。与 Span 对应,还有一个只读版本————ReadOnlySpan

本实例演示了用两种方法将某个字符串中的两个字符(“2”和“0”)转换为 int 数值 20,并且使用 Stopwatch 组件分布计算两种方法所消耗的时间。

【操作流程】

步骤1:新建控制台应用程序项目。

步骤2:声明并初始化一个字符串实例,稍后测试使用。

string str = "我家里养了20只猫";

步骤3:第一种处理方法,调用 Substring 方法取出“2”和“0”两个字符串,在通过 Parse 方法产生 int 实例。

Stopwatch sw1 = Stopwatch.StartNew();
for(int x = 0; x < 10000000; x++)
{
    int v = int.Parse(str.Substring(5, 2));
}
sw1.Stop();
Console.WriteLine($"常规方法:耗时 {sw1.ElapsedMilliseconds} ms");

步骤4:第二种方法,使用 Span,T 为 char 类型。调用 Slice 方法取出“2”和“0”两个字符串,在通过自定义代码将其转换为 int 值。

Stopwatch sw2 = Stopwatch.StartNew();
ReadOnlySpan span = str.ToCharArray();
for (int x = 0; x < 10000000; x++)
{
    int v = 0;
    var subspan = span.Slice(5, 2);
    for(int i = 0; i < subspan.Length; i++)
    {
        char ch = subspan[i];
        v = (ch - '0') + v * 10;
    }
}
sw2.Stop();
Console.WriteLine($"使用 Span:耗时 {sw2.ElapsedMilliseconds} ms");

由于 Span 实例中取出来的元素是 char 类型,为了能得到与字符串相对应的整数值,应该将其减去字符“0”的ASCII码。字符“0”的 ASCII 码是 48,以此类推,字符“1”的 ASCII 码 49,字符“2”的 ASCII 码 50,如果要使字符“2”与整数 2 对应,就需要 50 减去 48。

步骤5:运行应用程序项目,结果如下。

在这里插入图片描述

很明显采用Span的效率比较高。

注意:为了能让对比的效果更直观,本实例在要给 for 循环中将每种给处理方法的代码重复执行了 10000000 次。 在计时的时候,Stopwatch 组件自身也会消耗一定的 CPU 资源,因此代码执行所耗费的准确时间与 Stopwatch 所计算结果会有误差,仅用于参考。

多个Task同时操作ConcurrenBag集合

【导语】

ConcurrentBag 是一个泛型集合,它比较明显的优点是可以在多个线程上同时访问。并且该集合是无序的,即从集合中取出元素的顺序可能与放入的顺序不一样。

要向集合中添加元素可以调用 Add 方法。而取出元素则有两个方法可用:TryTake 方法取出元素然后把元素从集中删除,TryPeek 方法取出元素但是不会删除元素。

要判断 ConcurrentBag 集合中是否存在元素,一种方法是访问 Count 属性,它表示集合中包含元素的个数;另一种方法是访问 IsEmpty 属性,如果集合为空则返回 true

【操作流程】

步骤1:新建控制台应用程序项目。

步骤2:实例化 ConcurrentBag 集合,参数 T 为 int 类型。

ConcurrentBag bag = new ConcurrentBag();

步骤3:在项目生成的 Program.cs 文件头部的 using 指令块中,添加对以下命名空间的引用。

using System.Collections.Concurrent;
using System.Threading.Tasks;

步骤4:启动第一个 Task,向集合中添加元素。

Task t1 = Task.Run(() =>
{
    for (int k = 45; k < 51; k++)
    {
        Console.WriteLine("即将添加元素:{0}", k);
        bag.Add(k);
    }
});

步骤5:启动第二个 Task,从集合中取出元素,此 Task 在第一个 Task 执行之后执行。

Task t2 = t1.ContinueWith((task) =>
{
    while (!bag.IsEmpty)
    {
        if(bag.TryTake(out int item))
        {
            Console.WriteLine("已取出元素:{0}", item);
        }
    }
});

ContinueWith 方法指定在某个 Task 之后延续新的 Task。如果两个 Task 同时执行,由于集合的初始化状态是空的,会导致第二个 Task 中无法获取到元素。所以要让第一个 Task 先执行,这样在执行第二个 Task 时,就能保证集合中是有元素的。

步骤6:调用 WaitAll 方法,让当前线程暂定执行,等待上面两个 Task 任务执行完成再继续。

Task.WaitAll(t1, t2);

步骤7:运行应用程序项目,结果如下。

在这里插入图片描述

跨线程访问BlockingCollection集合

【导语】

BlockingCollection 集合允许多线程访问(添加或移除元素)。当集合中的元素数量达到容量上限时,添加操作会被阻止,直到集合中有元素被移除(重新获取到可用容量);同样的,当从集合中移除元素时,如果集合中无可用元素,那么移除操作会别阻止,直到集合中有新的元素加入。

多个线程可用同时调用 AddTryAdd 方法向集合中添加元素,也可以同时调用 TakeTryTake 方法移除元素。当调用 CompleteAdding 方法后,就不能在向集合中添加元素了,否则会引发异常。

【操作流程】

步骤1:新建控制台应用程序项目。

步骤2:创建 BlockingCollection 实例,T 为 int 类型。

using (BlockingCollection bc = new BlockingCollection())
{
    ...
}

BlockingCollection 类实现了 IDisposable 接口,若不再使用要将其释放。将初始化代码放在 using 语句块中,当代码离 using 块时会自动调用 Dispose 方法。

步骤3:在 using 代码块内部,创建第一个 Task,用于向集合中添加元素。

Task t1 = Task.Run(async () =>
{
    for (int k = 0; k < 5; k++)
    {
        int item = k + 1;
        Console.WriteLine("即将添加元素:{0}", item);
        bc.Add(item);
        await Task.Delay(650);
    }
    // 标记添加操作已完成
    bc.CompleteAdding();
});

在调用 CompleteAdding 方法后,集合被标志位已完成添加操作,此时所有线程均无法在向集合中添加元素。

步骤4:创建第二个 Task,用于从集合中移除元素。

Task t2 = Task.Run(() =>
{
    while (true)
    {
        if (bc.TryTake(out int item))
        {
            Console.WriteLine("取出元素:{0}", item);
        }
        // 是否退出循环
        if (bc.IsCompleted) break;
    }
});

当集合中的所有元素都被移除了(空集合),IsCompleted 属性会变为 true,此时就可以退出 while 循环了。

步骤5:调用 WaitAll 方法等待所有 Task 执行完成,然后释放上述两个 Task

Task.WaitAll(t1, t2);
t1.Dispose();
t2.Dispose();

步骤6:运行应用程序项目,结果如下。

在这里插入图片描述

总结

本文到这里就结束了,下一篇将介绍LINQ 的知识案例。

关注
打赏
1665926880
查看更多评论
立即登录/注册

微信扫码登录

0.1411s