• 多线程(一)——委托与多线程


    委托的使用

    委托的概念,简单匿名函数与lamda表达式使用

    委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用语句,同时使得程序具有更好的可扩展性。
    把一个方法作为参数传给另一个方法
    委托内部由三部分:1,_methodPtr,是一个方法指针,指向当前委托指向的方法的内存地址;
    2,_target,目标对象,如下

    Program p=new Program();
    del += p.AddStaticFunc1;
    
    • 1
    • 2

    中,方法指针指向的是AddStaticFunc1方法,目标对象就是p
    3、委托链,就是一个委托数组,定义委托实例AddDel del = new AddDel(AddStaticFunc);的背后就是创建这三个部分,当指向的方法为静态方法的时候,_target为null。
    执行的时候,从委托链的第0个索引到最后一个索引,因此多播委托返回的是最后一个方法的执行结果。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace 委托概念
    {
        internal class Program
        {
            //声明一个委托指向一个函数
            //委托指向的参数必须跟委托具有相同的签名
            public delegate void DelSayHi(string name);
            static void Main(string[] args)
            {
                //DelSayHi del = English;//new DelSayHi(English);
                //del("张三");
                //test("张三",Chinese);
                //test("李四", English);
                /*DelSayHi del=delegate (string s)
                 {
                     Console.WriteLine("中文" + s);
                 };*/
                DelSayHi del = (string s)=>
                {
                    Console.WriteLine("中文" + s);
                };
                //lamda表达式,匿名函数的简写形式 => goes to
                del("张三");
                Console.ReadKey();
            }
            public static void test(string name, DelSayHi del)
            {
                del(name);
            }
            public static void Chinese(string name)
            {
                Console.WriteLine("中文"+name);
            }
            public static void English(string name)
            {
                Console.WriteLine("英文" + name);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    委托的定义与使用

    委托的定义与使用:
    代码1:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApp1
    {
        //定义一个委托类型
        delegate int AddDel(int a, int b);
        internal class Program
        {
            static void Main(string[] args)
            {
                //定义一个委托的实例
                AddDel del = new AddDel(AddStaticFunc);
                //触发实例
                int r = del(3, 4);
                Console.WriteLine(r);
                Console.ReadKey();
            }
            //定义一个符合委托规范的方法
            public static int AddStaticFunc(int a,int b)
            {
                return a + b;
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    结果输出:7
    代码2:
    匿名函数使用

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace 委托
    {
        internal class Program
        {
            public delegate string DelProStr(string name);
            static void Main(string[] args)
            {// ProStrSYH(names);
                string[] names = { "abcdEFG", "HIjklMn" };
    
                ProStr(names, delegate(string name)
                {
                    return name.ToUpper();
                });
                //匿名函数,用delegate代替,只执行一次用
                for (int i = 0; i < names.Length; i++)
                {
                    Console.WriteLine(names[i]);
                }
                Console.ReadKey();
            }
    
            public static void ProStr(string[] name,DelProStr del)
            {
                for (int i = 0; i < name.Length; i++)
                {
                    name[i] = del(name[i]);
                }
            }
           /* public static string StrToUpper(string n)
            {
                return n.ToUpper();
            }
            public static string StrToLower(string n)
            {
                return n.ToLower();
            }
            public static string StrSYH(string name)
            {
                return "\"" + name + "\"";
            }*/
            /* public static void ProStrToUpper(string[] name)
             {
                 for(int i = 0; i < name.Length; i++)
                 {
                     name[i] = name[i].ToUpper();
                 }
             }
             public static void ProStrToLower(string[] name)
             {
                 for (int i = 0; i < name.Length; i++)
                 {
                     name[i] = name[i].ToLower();
                 }
             }
             public static void ProStrSYH(string[] name)
             {
                 for (int i = 0; i < name.Length; i++)
                 {
                     name[i] = "\"" + name[i]+ "\"";
                 }
             }*/
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    输出:
    ABCDEFG
    HIJKLMN

    多播委托

    多播委托1:

    Program p=new Program();
    del += p.AddStaticFunc1;
    public int AddStaticFunc1(int a, int b)
    {
        return a + b+1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    执行结果:8
    使用多播委托时,拿到的委托返回值是最后一个委托指向的方法的执行结果。
    多播委托2:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace 委托
    {
        //可以指向多个函数
        public delegate void DelTest();
        internal class Program
        {
            static void Main(string[] args)
            {
                DelTest del = T1;
                del += T2;
                del -= T1;
                del();
                Console.ReadKey();
            }
            public static void T1()
            {
                Console.WriteLine("T1");
            }
            public static void T2()
            {
                Console.WriteLine("T2");
            }
            public static void T3()
            {
                Console.WriteLine("T3");
            }
            public static void T4()
            {
                Console.WriteLine("T4");
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    执行结果:T2 T3 T4

    泛型委托

    简单例子:

    Func<int, int,int> funcDemo = new Func<int, int,int>(AddStaticFunc);
    int result=funcDemo(1,2);
    Console.WriteLine(result);
    public static int AddStaticFunc(int a,int b)
    {
        return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结果是3。
    Func原构造函数:

    public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
    
    • 1

    可以有很多个int,但是要有一个out,最后一项是用来约束返回值的。
    在这里插入图片描述

    匿名方法

    Func<int, int,int> funcDemo = delegate(int a,int b) { return a + b; };
    
    • 1

    Lamda表达式

    Func<int, int,int> funcDemo = (int a,int b)=> { return a + b; };Func<int, int,int> funcDemo = (int a,int b)=>  a + b;
    
    • 1
    • 2
    • 3

    例子:

    public delegate void DelOne();
    public delegate void DelTwo(string name);
    public delegate string DelThree(string name);
    static void Main(string[] args)
    {
        //DelOne del = delegate () { };
        DelOne del = () => { };
        //DelTwo del2 = delegate(string name) { };
        DelTwo del2 = (string name) =>{};
        /*DelThree del3 = (string name) =>{
            return name;
        };*/
        DelThree del3 = delegate (string name)
        {
            return name;
        };
        List<int> list=new List<int> { 1, 2, 3 };
        list.RemoveAll(n => n > 4);
        //lamda表达式
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    举例

    List<string> list = new List<string>()
    {
        "3","9","32","7","2"
    };
    var temp = list.Where(delegate (string a)
    {
        return a.CompareTo("6") < 0;
    });
    foreach (var v in temp)
    {
        Console.WriteLine(v);
    }
    Console.ReadKey();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    where是扩展方法,内部会遍历list集合,把每个元素传到委托里执行,如果返回true,就把元素选择出来,最后把所有满足条件的元素一起返回

    扩展方法

    三要素:静态类、静态方法、this关键字
    重新定义where方法
    定义:

    public static class Class1
    {
        public static List<T> myWhere<T>(this List<T> list,Func<T,bool> funcWhere)
        {
            List<T> result = new List<T>();
            foreach (var item in list)
            {
                if (funcWhere(item))
                {//调用委托,如果满足条件,加到集合中
                    result.Add(item);
                }
            }
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    调用:

    List<string> list = new List<string>(){ "3","9","32","7","2"};
    匿名方法:
    var temp = list.myWhere(delegate (string a)
    {
        return a.CompareTo("6") < 0;
    });
    lamda表达式
     var temp = list.myWhere<string>(a => a.CompareTo("6") < 0);
    方法泛型的约束可以省略,但是如果是显式的约束就必须满足约束。
    即<string>可以省略不写,就像var demo = "sss";一样,编译器会自动推断类型
    foreach (var v in temp)
    {
        Console.WriteLine(v);
    }
    Console.ReadKey();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    winform间窗体传值

    传统方法

    form1和form2两个类之间传值,在from1定义一个form2的对象,并将form2中文本框设置为外部可访问,如下所示。
    form1:

    public Form2 myForm2 { get; set; }
    private void button2_Click(object sender, EventArgs e)
    {
        myForm2.textBox2.Text = this.textBox1.Text;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    form2的designer中:

    public System.Windows.Forms.TextBox textBox2;
    
    • 1

    但是,对象内部的字段或者元素最好不要直接让外部访问,最好通过设置的方法控制一下。
    例如,设置为私有后,在子窗体定义函数,父窗体中的子窗体对象调用函数:

    public void SetText(string txt)
    {
        textBox2.Text = txt;
    }
    myForm2.SetText(this.textBox1.Text);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用委托

    主窗体发送消息:点击按钮执行委托,给委托传递参数(当前文本框的内容),子窗体想接收就注册主窗体的委托,把子窗体的方法注册到委托中,执行时,委托执行每个子窗体的方法
    主窗体发布,子窗体订阅
    主窗体:点击按钮,执行委托

    public Action<string> AfterMsgSend { get; set; }
    public Form2 myForm2 { get; set; }
    private void button2_Click(object sender, EventArgs e)
    {
    	 if(AfterMsgSend == null)
    	 {
    	     return;
    	 }
    	 AfterMsgSend(this.textBox1.Text);
    }
    private void Form1_Load(object sender, EventArgs e)
    {
    	 Form2 frm = new Form2();
    	 myForm2 = frm;
    	 //关注主窗体消息的变化
    	 AfterMsgSend+= frm.SetText;
    	 frm.Show();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    SetText是Form2的一个方法

    public void SetText(string txt)
    {
        textBox2.Text = txt;
    }
    
    • 1
    • 2
    • 3
    • 4

    事件方式实现

    //定义消息发布的事件
    public event EventHandler AfterMsgEvent;
    //EventHandler 是最常见的事件委托类型,子窗体需要符合它的规范
    事件触发:
    private void button2_Click(object sender, EventArgs e)
    {
        AfterMsgEvent(this, new TextChangeEvent()
        {
            Text = this.textBox1.Text
        });
    }
    TextChangeEvent是一个类,继承事件,含有属性Text
    public class TextChangeEvent:EventArgs 
    {
        public string Text { get; set; }
    }
    //加载父窗体时的事件
    AfterMsgEvent += frm.AfterParentFrmChange;
    Form2中函数:
    public void AfterParentFrmChange(object sender, EventArgs e)
    {
        //拿到父窗体传过来的值
        TextChangeEvent text=e as TextChangeEvent;
        this.SetText(text.Text);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    **区别:**委托是类型,事件是委托类型的一个特殊实例,事件只能在类的内部触发执行,安全

    发布订阅模式的实现

    发布订阅模式
    观察者模式
    指的是一个
    发布订阅:子窗体的个数和类型对主窗体没有影响
    如果不使用委托,在主窗体点击按钮,遍历集合,集合存放所有关心此主窗体消息变化的所有子窗体,这个集合相当于注册委托或事件方法
    定义一个接口类型:

    public interface Interface1
    {
        void SetText(string text);
    }
    
    • 1
    • 2
    • 3
    • 4

    子窗体:

    public partial class ChildFrm : Form,Interface1
    {//继承基类,实现接口
        public ChildFrm()
        {
            InitializeComponent();
        }
        public void SetText(string text)
        {//实现接口的方法
            this.textBox1.Text = text;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    父窗体:

    public partial class ParentFrm : Form
    {
        public List<Interface1> ChildFrmList { get; set; }
        public ParentFrm()
        {
            InitializeComponent();
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            //遍历所有关注主窗体消息变化的子窗体集合
            //调用集合中每个元素的方法
            if(ChildFrmList==null) return;
            //减少括号的层次,比先判断不是空再循环要好一些
            foreach(var item in ChildFrmList)
            {
                item.SetText(this.textBox1.Text);
            }
        }
    
        private void ParentFrm_Load(object sender, EventArgs e)
        {
            ChildFrm frm=new ChildFrm();
            this.ChildFrmList=new List<Interface1>();
            this.ChildFrmList.Add(frm);
            frm.Show();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    如果再多加一个管理窗体,加载时的执行代码为:

    //弹出主窗体
    ParentFrm parentForm = new ParentFrm();
    parentForm.Show();
    ChildFrm childForm = new ChildFrm();
    parentForm.ChildFrmList = new List<Interface1>();
    parentForm.ChildFrmList.Add(childForm); 
    childForm.Show();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样,就实现了主窗体和子窗体的分离解耦。

    文件流复习

    选择文件并读取

    using(OpenFileDialog ofd=new OpenFileDialog())
    {
        if(ofd.ShowDialog() != DialogResult.OK)
        {
            return;
        }
        //选中文件后,读取文件
        //1、
        //string x=File.ReadAllText(ofd.FileName, Encoding.Default);
        //直接写,如果文件太大会卡死
        //2、使用文件流
        using(FileStream fs=new FileStream(ofd.FileName, FileMode.Open, FileAccess.Read))
        {
            textBox1.Text = ofd.FileName;
            using(StreamReader sr=new StreamReader(fs))
            {
                while (!sr.EndOfStream)
                {
                    string line=sr.ReadLine();
                    this.textBox2.Text += line;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    写入文件

    using(SaveFileDialog save=new SaveFileDialog())
    {
        if(save.ShowDialog() != DialogResult.OK)
        {
            return;
        }
        //1、
        //File.WriteAllText(save.FileName,textBox2.Text,Encoding.Default);
        //2、
        /*using(StreamWriter sw=new StreamWriter(save.FileName,false,Encoding.Default,1024*1024))
        {//第二个参数是是否追加的意思,最后一个参数为缓冲区大小
            sw.Write(textBox2.Text);
            sw.Flush();//一举写入
        }*/
        //3、文件流写入
        using(FileStream fs=new FileStream(save.FileName, FileMode.OpenOrCreate, FileAccess.ReadWrite))
        {
            string x=textBox2.Text.Replace("\n","\r\n");
            
            byte[]data=Encoding.Default.GetBytes(x);
            //不读取换行,一行行的读写,因为x没有\r
            //fs.Write(data,0,data.Length);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    多线程

    一个线程就是一个进程里的代码执行流,每个线程都要指向一个方法体,方法执行完成之后,线程就释放。
    例如:

    Thread thread = new Thread
    (delegate ()
    {
        Console.WriteLine(DateTime.Now);
        Thread.Sleep(1000);
    });
    相当于
    Thread thread = new Thread(ThreadMethod);
    static void ThreadMethod()
    {
        Console.WriteLine(DateTime.Now);
        Thread.Sleep(1000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    进程执行是调用Start()方法,线程默认是前台线程。尽量设置为true

    thread.IsBackground = true;
    thread.Start();
    
    • 1
    • 2

    进程退出的标志:所有的前台线程都结束之后,后台线程不会阻塞进程的退出。
    程序执行时,Main函数是程序的入口,CLR一开始就默认创建一个主线程,主线程也是一个前台线程。

    Thread thread = new Thread(ThreadMethod);
    thread.IsBackground = true;
    thread.Start();
    while (true)
    {
        Console.WriteLine("from 主线程-----");
        Thread.Sleep(1000);
    }
    Console.ReadKey();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    执行结果:
    在这里插入图片描述
    主线程与创建的线程不停的执行,类似于俩个线程同时在执行。
    线程的其它属性:1、 IsAlive表示当前线程的状态,2、ManagedThreadId表示线程id
    如下:

    Console.WriteLine(thread.ManagedThreadId);
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);  
    
    • 1
    • 2

    3、优先级
    设置为最高都是建议操作系统设置为最高,不一定为最高;线程执行是告诉操作系统可以执行,自己准备好了。

    thread.Priority = ThreadPriority.Highest;
    
    • 1

    4、挂起:thread.Suspend();
    5、继续挂起的线程:thread.Resume();
    6、终止Abort,不得已才能用,直接终结:thread.Abort();
    7、等待thread线程执行完成:thread.Join();,执行代码的线程等待thread执行完成,参数为超时时间。
    例如:thread.Join(5000);后的代码执行结果为:
    在这里插入图片描述

    WinFrm与多线程

    创建一个Windows窗体,设置为控制台输出
    在这里插入图片描述
    如果不使用线程,显示窗体线程就是前台线程,卡死;
    如果设置了线程,使用lamda表达式输出内容,设置为前台线程,既可以控制台输出,主窗体也不卡死,关掉窗体,线程关闭。
    如果不设置线程为前台线程,那么关闭主窗体,控制台仍然执行。

    Thread thread = new Thread(() =>
    {
        while(true)
           Console.WriteLine(DateTime.Now.ToString());
    });
    thread.IsBackground = true;
    thread.Start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    跨线程访问控件

    线程访问主线程(主窗体)控件,设置非法跨线程为false;但是这个属性只是演示,真正项目不可以使用。

    Control.CheckForIllegalCrossThreadCalls = false;
    
    • 1

    正确访问方法:

    Thread thread = new Thread(() =>
    {
        if (button1.InvokeRequired)
        {//检查是否为别的线程创建的子控件,为true
            //找到创建控件的线程,执行方法
            button1.Invoke(new Action<string>(s => {
                this.button1.Text = s;
            }), DateTime.Now.ToString());
        }else   this.button1.Text = DateTime.Now.ToString();
        while(true)
           Console.WriteLine(DateTime.Now.ToString());
    });
    thread.IsBackground = true;
    thread.Start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    进程与线程

    操作系统分配的资源最小单位是进程,进程与进程之间互不影响,可以理解为一个程序的基本边界。
    进程是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法直接访问另 一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域 的。进程可以理解为一个程序的基本边界。
    CPU的其中一个核心,同一时间只能执行一个指令。
    一个线程就是一个进程里面的代码的执行流。每个线程都要指向一个方法体,方法执行完成后线程就释放。
    输出所有进程:

    var allPro = Process.GetProcesses();
    foreach (var pro in allPro)
    {
        Console.WriteLine(pro.ProcessName + "\t" + pro.Id);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    打开某个进程:

    Process.Start("notepad", "aaa.txt");
    
    • 1

    第二个参数是文件名,如果打开浏览器,是网站地址

  • 相关阅读:
    Nginx防盗链
    爬虫到底难在哪里?
    Web前端—小兔鲜儿电商网站底部设计及网站中间过渡部分设计
    Nginx + Tomcat 搭建负载均衡、动态分离
    用Python做了几个爬虫私活,赚了
    LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题
    实名认证在文旅出行行业的应用场景有哪些?
    Python进阶(五)-图形界面编程Tkinter(5)
    【狂神说】CSS3详解
    堆 超详细的带图总结
  • 原文地址:https://blog.csdn.net/a10750/article/details/126308906