C# 查漏补缺(二)

目录

1.泛型结构

1.1扩展方法和泛型类

1.2泛型委托  

2.协变和逆变

21.协变

2.2 逆变

3.迭代器

3.1IEnumerator接口

3.2 IEnumerable 接口

    3.3.使用yeild创建可枚举类型

4.Linq

4.1Join

4.2.let子句

4.3.into子句

5.异步编程

5.1线程知识点

5.2 ValueTask 

5.3 Task.Delay

5.4.BackgroundWorker

6. 异步编程旧模式

6.1介绍

6.2. BeginInvoke和EndIvoke

6.3 等待直到完成        

6.4 轮询模式

6.5 回调模式

6.6 计时器


1.泛型结构

1.1扩展方法和泛型类

public class Holder
    {
        T[] Vals = new T[3];
        public Holder(T t0,T t1,T t2)
        {
            Vals[0] = t0; Vals[1] = t1; Vals[2] = t2;
        }
        public T[] GetValues() => Vals;
    }
    public static class ExtendHolder
    {
        public static void Print(this Holder h)
        {
            var arr=h.GetValues();
            Array.ForEach(arr, x=>Console.Write(x+"  -   "));
        }
    }

1.2泛型委托  

 internal class GenericDelegate
    {
        public static void Test()
        {
            var myDel=new Func(Simple.PrintString);
            Console.WriteLine($"Total : {myDel(100, 1)}");
        }
    }
    public delegate TR Func(T1 p1,T2 p2);

    public class Simple
    {
        static public string PrintString(int p1,int p2)
        {
            int total = p1 + p2;
            return total.ToString();
        }
    }

2.协变和逆变

21.协变

先看一个例子:

internal class Convariance
    {
        public static Dog DogMaker() => new Dog();
        public static void Test()
        {
            Factory dogMaker = DogMaker;
            Factory animalMaker = dogMaker;
            Console.WriteLine(animalMaker().legs.ToString());
        }
    }
    class Animal { public int legs = 4; }
    class Dog:Animal { }
    delegate T Factory();

上面的代码会编译不通过:

 虽然Dog是Animal的派生类,但是委托Factory 没有从委托Factory派生,两个委托对象是同级的,没有直接关系,因此兼容性不适用。

仅将派生类型用作输出值与构造委托之间的常数关系叫作协变,为了让编译器知道这是我们的期望,必须使用out关键字标记委托声明中的类型参数。

    class Animal { public int legs = 4; }
    class Dog:Animal { }
    delegate T Factory();

2.2 逆变

与协变类似,但是又相反,协变是输出类型的代理,逆变则是为了输入类型,看下面的例子:

 internal class Contravariance
    {
        delegate void Action(T t);
        static void ActOnAnimal(Animal a) => Console.WriteLine(a.legs);
        public static void Test()
        {
            Action act1 = ActOnAnimal;
            Action act2 = act1;
            act2(new Dog());
        }
    }

同理,如果没有“in”,在act2赋值时会报错,因为Dog可以当作Animal类参数,但本身Action并不与Action有直接关系。这种在期望传入基类时允许传入派生类对象的特性叫作逆变。

3.迭代器

3.1IEnumerator接口

         实现了IEnumerator接口的枚举器包含3个函数成员:Current,MoveNext,以及Reset。

  • Current是返回序列中当前位置项的属性返回Object类型的引用,所以可以返回任何类型的对象
  • MoveNext 是吧枚举器位置前进到集合中下一项的方法,返回布尔值,指示的位置是有效为位置还是已经超过了序列的尾部。如果新的位置是有效的,方法返回true,否则false,枚举器的原始位置在序列中的第一项之前,因此MoveNext必须在第一次使用Current之前调用。
  • Reset是把位置重置为原始状态的方法

3.2 IEnumerable 接口

        可枚举类型是指实现了IEnumerable 接口的类,IEnumerable接口只有一个成员GetEnumerator方法,它返回对象的枚举器。看一下综合例子:

  internal class IEnumeratorStudy
    {
        public static void Test()
        {
            Spectrum spectrum=new Spectrum();
            foreach(var s in spectrum)
                Console.WriteLine(s);
        }
    }
    class ColorEnumerator:IEnumerator
    {
        string[] colors;
        int position = -1;
        public ColorEnumerator(string[] theColors)
        {
            colors = new string[theColors.Length];
            for (int i = 0; i < theColors.Length; i++)
                colors[i] = theColors[i];
        }
        public object Current
        {
            get
            {
                if (position == -1)
                    throw new InvalidOperationException();
                if(position>=colors.Length)
                    throw new InvalidOperationException();
                return colors[position];
            }
        }
        public bool MoveNext()
        {
            if(position

    3.3.使用yeild创建可枚举类型

使用yeild很容器创建可枚举类型(不是枚举器),后台会帮我们做很多事情。

 internal class YeildStudy
    {
        public static void Test()
        {
            MyClass mc=new MyClass();
            foreach(var shade in mc)
            {
                Console.WriteLine(shade);
            }
            //使用类枚举器方法
            foreach(var shade in mc.BlackAndWhite())
            {
                Console.WriteLine(shade);
            }    
        }

    }
    public class MyClass
    {
        public IEnumerator GetEnumerator()
        {
            IEnumerable myEnumerable = BlackAndWhite();
            return myEnumerable.GetEnumerator();
        }
        public IEnumerable BlackAndWhite()
        {
            yield return "Black";
            yield return "Gray";
            yield return "white";
        }
    }

4.Linq

4.1Join

Linq中的join子句和SQL中的join子句很相似。不同的是linq的join子句不但可以在数据库的表上执行廉洁,还可以在集合对象上进行这个操作。廉洁操作接受两个集合,然后创建一个临时的对象集合,其中每个对象包含两个原始集合对象中的所有字段。

举个例子,倘若你有一个学生表,一个选课表:

             Student[] students = new Student[]
            {
                new Student(){Id=1,Name="张三"},
                new Student{Id=2,Name="李四"},
                new Student(){Id=3,Name="王五"}
            };

            CourseStudent[] courses = new CourseStudent[]
            {
                new CourseStudent(){ CourseName="美术",StuId=1},
                new CourseStudent(){CourseName="美术",StuId=2},
                new CourseStudent(){CourseName="历史",StuId=1},
                new CourseStudent(){CourseName="历史",StuId=3},
                new CourseStudent(){CourseName="物理",StuId=3}
            };

现在你要找出选了历史的同学的姓名,怎么写呢?用join

            var query = from s in students
                        join c in courses
                        on s.Id equals c.StuId
                        where c.CourseName == "历史"
                        select s.Name;
            Array.ForEach(query.ToArray(),x=>Console.WriteLine(x));

这样写就可以找出来了。其实也不一定非要用join,下面这个办法也是可以的

                        var query2= from s in students
                        from c in courses
                        where c.CourseName=="历史" && s.Id==c.StuId
                        select s.Name;

可能会存在效率问题,个人感觉join的效率会更高。

但是from子句在两个集合没有连接关系时更实用,看个例子:

        public static void Test2()
        {
            var A=new[] {3,4,5,6};
            var B = new[] { 6, 7, 8, 9 };
            var ans=from a in A
                    from b in B
                    where a>4 && b<=8
                    select new {a,b,sum=a+b}; //    匿名对象
            Array.ForEach(ans.ToArray(),x=>Console.WriteLine(x));
        }

输出为:

{ a = 5, b = 6, sum = 11 }
{ a = 5, b = 7, sum = 12 }
{ a = 5, b = 8, sum = 13 }
{ a = 6, b = 6, sum = 12 }
{ a = 6, b = 7, sum = 13 }
{ a = 6, b = 8, sum = 14 }

4.2.let子句

        let子句接受一个表达书运算,并且吧它赋值给一个需要在其它运算中使用的标识符。

拿上面的例子,我们可以优化匿名对象:

                    var ans=from a in A
                    from b in B
                    let sum=a+b
                    where a>4 && b<=8
                    select new {a,b,sum}; //    匿名对象

效果和上面是一模一样的。

值得一提的是,查询表达式可以后任意个where子句,我们换个条件:

            var ans2=from a in A
                    from b in B
                    let sum=a*b
                    where a%2==0
                    where sum<40
                    select new {a,b,sum};
            Array.ForEach(ans2.ToArray(), x => Console.WriteLine(x));

如果条件很多,建议用多个where,而不是与或非,这样更清晰一些。

4.3.into子句

 查询延续子句可以接受查询的一部分的结果,并赋予一个名字,从而可以在查询的另一部分中使用。看个例子:

            var groupA=new [] {3,4,5,6};
            var groupB=new [] {4,5,6,7};
            var someInts = from a in groupA
                           join b in groupB on a equals b
                           into groupAB
                           from c in groupAB
                           select c;
            Array.ForEach(someInts.ToArray(), x => Console.WriteLine(x));

5.异步编程

5.1线程知识点

  • 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束
  • 线程可以派生其他线程,因此任意时刻,一个进程都可能包含不同状态的多个线程,他们执行程序的不同部分
  • 如果一个进程拥有多个线程,他们共享进程的资源
  • 系统为处理器执行所调度的单元是线程,不是进程。

5.2 ValueTask 

ValueTask:这是一个值对象,它与Task类似,但 用于任务结果可能已经可用的情况。因为它是一个值类型,所以放在栈上,而无需像Task对象那样在堆山分配空间,因此某些情况下可以提高性能。

5.3 Task.Delay

        Task.Delay 方法创建一个Task对象,该对象将暂停其在线程中的处理,并在一定时间之后完成。与Tread.Sleep阻塞线程不同的是,Task.Delay不会阻塞其它线程。看下面例子:

首先是没有await等待:

    public class Sample
    {
        Stopwatch sw=new Stopwatch();
        public void Run()
        {
            Console.WriteLine("Caller:Before Call");
            ShowDelayAsync();
            Console.WriteLine("Caller: After call");

        }
        private async void ShowDelayAsync()
        {
            sw.Start();
            Console.WriteLine($"  Before Delay: {sw.ElapsedMilliseconds}");
            Task.Delay(1000);
            Console.WriteLine($"  After Delay: {sw.ElapsedMilliseconds} ");
        }
    }

运行结果:

Caller:Before Call
  Before Delay: 0
  After Delay: 3
Caller: After call

很显然没有等待,Task.Delay是异步执行。加上等待之后呢?

        private async void ShowDelayAsync()
        {
            sw.Start();
            Console.WriteLine($"  Before Delay: {sw.ElapsedMilliseconds}");
            await Task.Delay(1000);
            Console.WriteLine($"  After Delay: {sw.ElapsedMilliseconds} ");
        }

结果就是我们想要的了:

Caller:Before Call
  Before Delay: 0
Caller: After call
  After Delay: 1021

如果用sleep函数代替:

        private async void ShowDelayAsync()
        {
            sw.Start();
            Console.WriteLine($"  Before Delay: {sw.ElapsedMilliseconds}");
            // await Task.Delay(1000);
            Thread.Sleep(1000);
            Console.WriteLine($"  After Delay: {sw.ElapsedMilliseconds} ");
        }

结果如下:

Caller:Before Call
  Before Delay: 0
  After Delay: 1005
Caller: After call

虽然也满足了延迟,但是整个进程都停滞了,所以主线程的顺序也改变了。

5.4.BackgroundWorker

3个事件,用于发送不同的程序事件和状态,你需要写这些事件的处理方法来执行合适的程序的行为:

  • 在后台线程开始的时候触发DoWork
  • 在后台任务汇报进度的时候触发ProgressChanged
  • 在后台工作线程退出的时候触发RunWorkerComplete        
 // 摘要:
        //     Occurs when System.ComponentModel.BackgroundWorker.RunWorkerAsync is called.
        public event DoWorkEventHandler? DoWork   
        //
        // 摘要:
        //     Occurs when System.ComponentModel.BackgroundWorker.ReportProgress(System.Int32)
        //     is called.
        public event ProgressChangedEventHandler? ProgressChanged     
        //
        // 摘要:
        //     Occurs when the background operation has completed, has been canceled, or has
        //     raised an exception.
        public event RunWorkerCompletedEventHandler? RunWorkerCompleted
   

三个方法用于开始行为或改变状态:

  • 调用RunWorkerAsync方法获取后台线程并且执行DoWork事件处理程序
  • 调用CancelAsync方法吧CancellationPending属性设置为true,Dowork事件处理程序需要检查这个属性来决定是否停止该程序
  • DoWork事件处理程序,在希望向主线程汇报进度的时候,调用ReportProgress方法。

第一个函数DoWork是必须的,因为它包含后台线程执行的代码。在DoWork函数中 通过调用ReportProgress方法与主线程通信,届时将处罚ProgressChanged事件,主线程可以用附加到ProgressChanged事件上的处理程序处理事件。

附加到RunWorkerComplete事件的处理程序应该包含在后台线程完成DoWork事件处理程序的执行之后需要执行的代码。

三个委托如下:

public delegate void DoWorkEventHandler(object? sender, DoWorkEventArgs e);

public delegate void ProgressChangedEventHandler(object? sender, ProgressChangedEventArgs e);

public delegate void RunWorkerCompletedEventHandler(object? sender, RunWorkerCompletedEventArgs e);

具体讲解可以看这个博客;

下面是一个典型的例子:

    internal class BackgroundWorkerStudy
    {
        public static void Test()
        {
            SimpleWorker simpleWorker = new SimpleWorker();
            Task.Run(() => simpleWorker.Start());
            while (true)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("enter anykey to over background task!");
                var s = Console.ReadLine();
                simpleWorker.Cancel();
                break;
            }
            Console.WriteLine("Input anykey to exit!");
            Console.ReadLine();
        }
    }
    public class SimpleWorker
    {
        BackgroundWorker bgWorker = new BackgroundWorker();

        public SimpleWorker()
        {
            bgWorker.WorkerReportsProgress = true;
            bgWorker.WorkerSupportsCancellation = true;

            bgWorker.DoWork += DoWork_Handler;
            bgWorker.ProgressChanged += ProgressChange_Handler;
            bgWorker.RunWorkerCompleted += RunWorkCompleted_Handler;
        }

        private void DoWork_Handler(Object sender, DoWorkEventArgs args)
        {
            var worker = sender as BackgroundWorker;
            for (int i = 1; i <= 100; i++)
            {
                if (worker.CancellationPending)
                {
                    args.Cancel = true;
                    break;
                }
                else
                {
                    worker.ReportProgress(i * 10);
                    Thread.Sleep(1000);
                }
            }
        }
        private void ProgressChange_Handler(Object sender, ProgressChangedEventArgs args)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine($"Now it is running: {args.ProgressPercentage}");
            for (int i = 0; i < args.ProgressPercentage / 10; i++)
            {
                Console.Write("*");
            }
            Console.WriteLine();
            Console.ForegroundColor = ConsoleColor.Magenta;
            
        }
        private void RunWorkCompleted_Handler(object sender, RunWorkerCompletedEventArgs args)
        {
            if (args.Cancelled)
                Console.WriteLine("Progress was cancled!");
            else
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine("Pregress Completed normally");
            }

        }
        public void Cancel()
        {
            bgWorker.CancelAsync();
        }

        public void Start()
        {
            if (!bgWorker.IsBusy)
                bgWorker.RunWorkerAsync();
        }
    }

6. 异步编程旧模式

6.1介绍

        现在有了await/async之后,我们编写异步程序基本上都是用这个。然而你仍然有可能需要使用旧的模式来生产异步代码。学习这种旧模式面对async/await特性会有更深刻的认识。

        如果委托对象在调用列表中只有一个方法,它就可以异步执行这个方法。委托类有两个方法:BeginInvoke和EndInvoke,它们就是用这个来实现目的的。

当调用委托的BeginInvoke方法时,他开始在一个独立的线程上执行引用方法。之后立即返回原线程,原始线程可以继续,而引用方法会在线程池的线程中并行执行。

当程序希望获取已完成的异步方法的结果时,可以检查BeginInvoke返回IAsyncResult的IsCompleted属性,或调用委托的EndInvoke方法来等待委托完成。

有三种调用模式:

C# 查漏补缺(二)_第1张图片

  1. 在等待直到完成模式中,在发起了异步方法以及做了一些其他处理之后,原始线程就中断并且等异步方法完成之后再继续。
  2. 在轮询(polling)模式中,原始线程定期检查发起的线程是否完成,如果没有则可以继续做一些其他事情
  3. 在回调模式中,原始线程一直执行没无须等待或检查发起的线程是否完成。在发起的线程中的引用方法完成之后美发器的线程就会调用回调方法,由回调方法调用EndInvoke之前处理异步方法的结果。

6.2. BeginInvoke和EndIvoke

        在调用BeginInvoke时,参数列表中的实际参数组成如下:

  • 引用方法所需要的参数
  • 两个额外的参数,callback参数和state参数

BeginInvoke从线程池中获取一个线程并且让引用方法在新的线程中开始运行。

BeginInvoke返回一个实现IAsyncResult接口的对象的引用。这个接口引用包含了在线程池中运行异步方法的当前状态,然后原始线程可以继续执行。

EndInvoke方法用来获取由一步方法调用返回的值,并且释放线程使用的资源。EndInvoke有如下的特性:

它接受BeginInvoke方法返回的IAsyncResult对象的引用作为参数,并找到它关联的线程。如果线程池的线程已经退出,则做下面2件事情:

  1. 清理退出线程的状态并释放资源
  2. 找到引用方法返回的值并把它作为返回值返回

如果调用EndInvoke时,线程还在运行,调用线程就会停止并等待它完成,然后再清理并返回值。如果异步方法触发了异常,则调用EndInvoke时会抛出异常。看个简单的例子:

    public class AsyncInvoke
    {
        delegate long Mydele(int x, int y);
        public AsyncInvoke()
        {
        }
        private static long Sum(int x, int y) => x + y;

        public static void Test()
        {
            Mydele del = new Mydele(Sum);
            IAsyncResult result = del.BeginInvoke(3, 4, null, null);
            long ans = del.EndInvoke(result);
            Console.WriteLine($"ans: {ans}");
        }
    }

6.3 等待直到完成        

    public class AsyncInvoke
    {
        delegate long Mydele(int x, int y);
        public AsyncInvoke()
        {
        }
        private static long Sum(int x, int y) => x + y;

        private static long Sum2(int x,int y)
        {
            Console.WriteLine("         Inside Sum");
            Thread.Sleep(100);
            return x + y;
        }
        public static void Test2()
        {
            Mydele d = new Mydele(Sum2);
            Console.WriteLine("Before BeginInvoke");
            var iar = d.BeginInvoke(2, 4, null, null);//开始异步调用
            Console.WriteLine("After BeginInvoke");
            Console.WriteLine("Doing Stuff");
            long result = d.EndInvoke(iar);
            Console.WriteLine($"After EndInvoke:  {result}");

        }
    }

代码输出如下:

Before BeginInvoke
After BeginInvoke
Doing Stuff
         Inside Sum
After EndInvoke:  6

6.4 轮询模式

        AsyncResult对象包含一个叫作AsyncDelegate的属性,它返回一个指向被调用来启动异步方法的委托的引用,但是这个属性是类对象的一部分而不是接口的一部分。

        IsCompleted属性返回一个布尔值,表示一步方法是否完成。AysncState属性返回对象的一个引用,作为BeginInvoke方法调用时的State参数。因此可以利用这个参数实现轮询。

        public static void Test3()
        {
            Mydele d = new Mydele(Sum2);
            IAsyncResult iat = d.BeginInvoke(4, 3, null, null);
            Console.WriteLine("After BeginInvoke!");
            while(!iat.IsCompleted)
            {
                Console.WriteLine("Not done!");
                for (int i = 0; i < 1000000; i++)
                    ;
            }
            Console.WriteLine("Done!");
            long result = d.EndInvoke(iat);
            Console.WriteLine($"Result: {result}");
        }

6.5 回调模式

        BeginInvoke参数列表中最后两个额外参数由回调方法使用。

第一个参数callback是回调方法的名字

第二个参数state可以是null,或要传入回调的一个对象引用。可以哦通过IAsyncResult参数的AsyncState属性来获取这个对象,参数的类型是Object。

        回调方法:

void AsyncCallBack(IAsyncResult iar)
        private static void CallWhenDone(IAsyncResult iar)
        {
            Console.WriteLine("        Inside CallWhenDone.");
            AsyncResult ar = (AsyncResult)iar;
            var del = (Mydele)ar.AsyncDelegate;
            long result = del.EndInvoke(iar);
            Console.WriteLine($"         The result is {result}");
        }
        public static  void Test4()
        {
            Mydele d = new Mydele(Sum2);
            Console.WriteLine("Before BeginInvoke");
            IAsyncResult iat = d.BeginInvoke(4, 3, CallWhenDone, null);
            Console.WriteLine("Doing more work in Main");
            Thread.Sleep(1000);
            Console.WriteLine("Done with Main. Exiting");
        }

6.6 计时器

        .Net 有好几个可以用的Timer类,这里主要介绍System.Threading中的那个

C# 查漏补缺(二)_第2张图片

 可以看到Timer有好多种构造函数,看个简单的例子:

     internal class TimerStudy
    {
        int TimesCalled = 0;
        void Display(object state)
        {
            Console.WriteLine($"{(string)state} {++TimesCalled}");
        }
        public static void Test()
        {
            TimerStudy timerStudy = new TimerStudy();
            //2秒后第一次调用,每秒重复一次
            Timer timer = new Timer(timerStudy.Display, "Processing timer event", 2000, 1000);
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("计时器开始...");           
            Console.ReadLine();
        }
    }

可以使用change方法来改变他的开始时间和周期,但是一旦计时器启动之后就不能更改了。

        此外,还有2个计时器:

  1. system.Windows.Forms.Timer 这个类在Windows Form应用程序中使用,用来定期吧WM_TIMER消息放到程序的消息队列中,当程序重队列获取消息后,它会在猪用户接口线程中同步处理程序,这对Windows Form应用程序来说很重要
  2. System.Timers.timer 这个类更复杂,包含很多成员,使我们可以通过属性和方法来操作计时器。

你可能感兴趣的:(C#,c#,linq,蓝桥杯)