当前位置: 首页 > news >正文

C# 学习笔记--个人学习使用 <2>

C# 学习笔记

  • Chapter 2 比较硬的基础部分
    • Section 1 委托
      • Part 1 Action 与 func 委托的示例
      • Part 2 自定义委托
      • Part 3 委托的一般使用
      • Part 4 委托的高级使用
      • Part 5 适时地使用接口 Interface 取代一些对委托的使用
    • Section 2 事件
      • Part 1 初步了解事件
      • Part 2 事件的应用
      • Part 3 事件的声明
      • Part 4 澄清
    • Section 3 类
      • Part 1 类的声明
      • Part 2 类的继承
      • Part 3 成员的继承与访问
      • Part 4 面向对象的实现风格
      • Part 5 重写与多态
    • Section 4 类的扩展知识
      • Part 1 抽象类
      • Part 2 接口
      • Part 3 单元测试与依赖倒置原则
      • Part 4 接口隔离原则
      • Part 5 反射 (待补充)
      • Part 6 泛型 Generic (待补充)



Chapter 2 比较硬的基础部分


Section 1 委托

什么是委托?

  • 委托 Delegate 是函数指针的升级版
  • Delegate 的意思是,这有一件事情,我不亲自去做,而是交给别人去做,也就是间接地去做;
#include <studio.h>int Add(int a, int b)
{int result = a + b;return result
}int Sub(int a, int b)
{int result = a - b;return result
}int main()
{int x = 100;int y = 200;int z = 0;z = Add(x, y);printf("%d+%d=%d", x, y, z);z = Sub(x, y);printf("%d-%d=%d", x, y, z);system("pause");return 0;
}

我们可以看到输出结果如下:

>> 100+200=300
>> 100-200=-100
>> Press any key to continue ...

在这个例子里,是通过函数的名字,来调用,是直接调用

#include <studio.h>typedef int (* Calc)(int a, int b); // 函数指针,并且定义为一种类型int Add(int a, int b)
{int result = a + b;return result
}int Sub(int a, int b)
{int result = a - b;return result
}int main()
{int x = 100;int y = 200;int z = 0;Calc funcPoint1 = &Add;Calc funcPoint2 = &Sub;	z = funcPoint1(x, y);printf("%d+%d=%d", x, y, z);z = funcPoint2(x, y);printf("%d-%d=%d", x, y, z);system("pause");return 0;
}

我们可以看到输出结果如下:

>> 100+200=300
>> 100-200=-100
>> Press any key to continue ...

可以看到输出结果是一样的,这就说明了间接调用和直接调用的效果是一样的,这就是C语言中的函数指针;

  • 一切皆地址
    • 变量(数据)是以某个地址为起点的一段内存中所存储的值;
    • 函数(算法)是以某个地址为起点的一段内存中所存储的一组机器语言指令;
  • 直接调用与间接调用
    • 直接调用:通过函数名来调用函数,处理器通过函数名直接获得函数所在的地址并开始执行 -> 返回;
    • 间接调用:通过函数指针来调用函数,处理器通过读取函数指针存储的值获得函数所在地址并开始执行 -> 返回;
  • Java 中没有委托相对应的功能实体;
  • 委托的简单实用
    • Action 委托;Void类型用
    • Func 委托;有参数的用

Part 1 Action 与 func 委托的示例

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Calculator calculator = new Calculator();Action action = new Action(calculator.Report); // 注意这里没有圆括号,这里只需要方法名,而不是调用方法calculator.Report(); // 直接调用action.Invoke(); // 间接调用,模仿函数指针的写法action(); // 间接调用,简洁的写法// 参数,参数,返回类型Func<int, int, int> func = new Func<int, int, int>(calculator.Add); Func<int, int, int> func2 = new Func<int, int, int> (calculator.Sub);int x = 100;int y = 200;int z = 0;// 间接调用,函数指针式的写法z = func.Invoke(x, y);Console.WriteLine(z);z = func2.Invoke(x, y);Console.WriteLine(z);// 间接调用,简洁的写法z = func(x, y);Console.WriteLine(z);z = func2(x, y);Console.WriteLine(z);}}class Calculator{public void Report(){Console.WriteLine("I have 3 methods");}public int Add(int a, int b){ int result = a + b;return result;}public int Sub(int a, int b){int result = a - b;return result;}}
}

运行上面的程序可以获得如下的输出:

I have 3 methods
I have 3 methods
I have 3 methods
300
-100
300
-100

Part 2 自定义委托

  • 由于委托是一种类 class,类是一种数据类型,且是引用类型的数据类型,委托可以声明变量和声明实例;
  • 委托的声明方式与一般的类的声明方式并不相同,更像是 C/C++ 中函数指针的声明方式;

下面这个例子是自定义委托的声明与使用;

namespace ConsoleHelloWorld
{public delegate double Calc(double x, double y);// delegate 是类,需要声明在名称空间体里面;// public 是访问范围,delegate 是告诉编译器要声明一个委托// 第一个 double 是目标方法的返回值类型// 然后 Calc 是委托的名字// 后面的圆括号里面是目标方法的参数列表// 到此自定义委托类型声明完成class Program{static void Main(string[] args){Calculator calculator = new Calculator();// 传递的方法的参数列表必须和声明时一样,返回类型也必须一致Calc calc1 = new Calc(calculator.Add); Calc calc2 = new Calc(calculator.Sub);Calc calc3 = new Calc(calculator.Mul);Calc calc4 = new Calc(calculator.Div);double a = 100;double b = 200;double c = 0;c = calc1.Invoke(a, b);Console.WriteLine(c);c = calc2.Invoke(a, b);Console.WriteLine(c);c = calc3.Invoke(a, b);Console.WriteLine(c);c = calc4.Invoke(a, b);Console.WriteLine(c);}}class Calculator{// 有四个方法,除了名字不同,返回值类型和参数列表都是一样的public double Add(double x, double y){ return x + y;}public double Sub(double x, double y){return x - y;}public double Mul(double x, double y){return x * y;}public double Div(double x, double y){return x / y;}}
}

运行上面的代码,可以获得以下的输出:
在这里插入图片描述

当我们自定义委托的时候,需要注意几点:

  • 委托与所封装的方法必须保持“类型兼容”
  • 声明委托的时候不要放错位置,委托是类,需要声明在名称空间体里面,放错了可能导致运行不了或成为嵌套类;

在这里插入图片描述
上图可以看到,第一行是委托的声明,下面四行是与之兼容的方法;

Part 3 委托的一般使用

在工作中,一般是把委托当做参数传到另一个方法里去,这样做的好处可以间接调用委托所封装的方法,形成一个动态调用方法的结构;

  • 模版方法,写了一个方法,通过传进来的委托参数,借用指定的外部方法来产生结果;
    • 相当于 填空题
    • 常位于代码中部
    • 委托有返回值
    • 相当于写了一个方法,是模版,这个模版里有一处是不确定的,其他地方是确定好的,这个不确定的部分就靠传进来的委托类型的参数所包含的方法来填补;
  • 回调方法 callback,调用制定的外部方法;
    • 相当于流水线
    • 常位于代码末尾
    • 委托没有返回值,通常用来处理一些末尾的工作;

下面展示的使模板方法的使用:

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){ProductFactory productFactory = new ProductFactory();WrapFactory wrapFactory = new WrapFactory();Func<Product> func1 = new Func<Product>(productFactory.MakePizza);Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);Box box1 = wrapFactory.WrapProduct(func1);Box box2 = wrapFactory.WrapProduct(func2);Console.WriteLine(box1.Product.Name);Console.WriteLine(box2.Product.Name);}}class Product{ public string Name { get; set; }    }class Box{ public Product Product { get; set; }    }class WrapFactory{public Box WrapProduct ( Func<Product> getProduct ){// 模板方法Box box = new Box();// 执行传进来的委托所封装的方法,这就是间接调用Product product = getProduct.Invoke(); // 获取产品,将产品装入 Boxbox.Product = product; return box;// 写成模版方法的好处是,Product类,Box类还有WrapFactory类都不需要在修改,// 只需要扩展产品工厂,让其产出更多的产品,不管生产哪种产品的方法,// 只需要将该方法封装在委托类型的对象里,传给模版方法,这个模版方法一定可以将// 产品包装成箱子返回回来,极大地实现代码的重复使用}}class ProductFactory{public Product MakePizza(){ Product product = new Product();product.Name = "Pizza";return product;}public Product MakeToyCar(){ Product product = new Product();product.Name = "Toy Cat";return product;}}
}

下面展示的是回调方法的使用:

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){ProductFactory productFactory = new ProductFactory();WrapFactory wrapFactory = new WrapFactory();Func<Product> func1 = new Func<Product>(productFactory.MakePizza);Func<Product> func2 = new Func<Product>(productFactory.MakeToyCar);Logger logger = new Logger();Action<Product> log = new Action<Product>(logger.Log);Box box1 = wrapFactory.WrapProduct(func1, log);Box box2 = wrapFactory.WrapProduct(func2, log);Console.WriteLine(box1.Product.Name);Console.WriteLine(box2.Product.Name);}}class Logger{public void Log(Product product){// Log 以回调的形式传进模版的方法里Console.WriteLine("Product '{0}' created at {1}. Price is {2}.", product.Name, DateTime.UtcNow, product.Price);}}class Product{ public string Name { get; set; }public double Price { get; set; }}class Box{ public Product Product { get; set; }    }class WrapFactory{public Box WrapProduct ( Func<Product> getProduct, Action<Product> logCallback){// 模板方法Box box = new Box();// 执行传进来的委托所封装的方法,这就是间接调用Product product = getProduct.Invoke(); // 获取产品,将产品装入 Boxif (product.Price >= 50){ logCallback(product);}box.Product = product; return box;// 写成模版方法的好处是,Product类,Box类还有WrapFactory类都不需要在修改,// 只需要扩展产品工厂,让其产出更多的产品,不管生产哪种产品的方法,// 只需要将该方法封装在委托类型的对象里,传给模版方法,这个模版方法一定可以将// 产品包装成箱子返回回来,极大地实现代码的重复使用}}class ProductFactory{public Product MakePizza(){ Product product = new Product();product.Name = "Pizza";product.Price = 20;return product;}public Product MakeToyCar(){ Product product = new Product();product.Name = "Toy Cat";product.Price = 120;return product;}}
}

无论是模版方法还是回调方法,都使用委托类型的参数封装一个外部的方法,然后把这个方法传进方法的内部进行间接调用, 这个就是委托的常规用法。
委托如果被滥用的后果非常危险;

  • 这时一种方法级别的紧耦合,现实工作中要慎之又慎;
  • 使可读性下降、debug难度增加;
  • 把委托回调、异步调用和多线程纠缠在一起,会让代码难以维护和阅读,是灾难级的;
  • 委托的使用不当有可能造成内存泄漏和程序性能下降;

Part 4 委托的高级使用

在这里插入图片描述

多播委托指的是一个委托内部封装了不止一个方法,下面是例子:

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student student1 = new Student() { Id = 1, PenColor = ConsoleColor.Yellow };Student student2 = new Student() { Id = 2, PenColor = ConsoleColor.Green };Student student3 = new Student() { Id = 3, PenColor = ConsoleColor.Red };Action action1 = new Action(student1.DoHomework);Action action2 = new Action(student2.DoHomework);Action action3 = new Action(student3.DoHomework);// 多播委托的写法:action1 += action2; // 将 aciton2 合并到 action1action1 += action3;action1.Invoke();// 多播委托的执行顺序是按照你封装方法的顺序执行的}}class Student{ public int Id { get; set; }public ConsoleColor PenColor { get; set; }public void DoHomework(){ for (int i = 0; i < 5; i++) {Console.ForegroundColor = this.PenColor;Console.WriteLine("Student {0} doing homework {1} hours.", this.Id, i);Thread.Sleep(1000); // 线程暂停一秒钟}}}
}

多播委托
隐式异步调用

  • 异步调用:与同步调用是相对的,
    • 同步:你做完了,我在你的基础上接着做;
    • 异步:咱们两个同时做,也就是各做各的;
  • 同步调用与异步调用的对比
    • 每一个运行的程序是一个进程 process
    • 每一个进程可以有一个或者多个线程 thread,第一个线程叫做主线程,之外的是分支线程
    • 同一个线程内调用方法的时候,是前一个执行完,后一个才能执行,叫做同步调用;
    • 异步调用的底层机理是多线程
    • 同步调用是单线程串行调用,异步调用是多线程并行调用;

下面是同步调用的异步调用的例子

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Student student1 = new Student() { Id = 1, PenColor = ConsoleColor.Yellow };Student student2 = new Student() { Id = 2, PenColor = ConsoleColor.Green };Student student3 = new Student() { Id = 3, PenColor = ConsoleColor.Red };// 直接同步调用student1.DoHomework();student2.DoHomework();student3.DoHomework();Console.WriteLine("=============================================");Action action1 = new Action(student1.DoHomework);Action action2 = new Action(student2.DoHomework);Action action3 = new Action(student3.DoHomework);// 使用委托的隐式异步调用action1.BeginInvoke(null, null);action2.BeginInvoke(null, null);action3.BeginInvoke(null, null);Console.WriteLine("=============================================");// 使用委托的显式异步调用Task task1 = new Task(new Action(student1.DoHomework));Task task2 = new Task(new Action(student2.DoHomework));Task task3 = new Task(new Action(student3.DoHomework));task1.Start();task2.Start();task3.Start();Console.WriteLine("=============================================");// 单播委托的间接同步调用action1.Invoke();action2.Invoke();action3.Invoke();Console.WriteLine("=============================================");// 多播委托的间接同步调用action1 += action2;action2 += action3;action1();Console.WriteLine("=============================================");}}class Student{ public int Id { get; set; }public ConsoleColor PenColor { get; set; }public void DoHomework(){ for (int i = 0; i < 5; i++) {Console.ForegroundColor = this.PenColor;Console.WriteLine("Student {0} doing homework {1} hours.", this.Id, i);Thread.Sleep(1000); // 线程暂停一秒钟}}}
}

Part 5 适时地使用接口 Interface 取代一些对委托的使用

委托使用不当回提高代码的维护难度,使用接口可以避免这些不必要的麻烦还可以获得相同的功能;

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){IProductFactory pizzaFactory = new PizzaFactory();IProductFactory toycarFactory = new ToyFactory();WrapFactory wrapFactory = new WrapFactory();Logger logger = new Logger();Action<Product> log = new Action<Product>(logger.Log);Box box1 = wrapFactory.WrapProduct(pizzaFactory, log);Box box2 = wrapFactory.WrapProduct(toycarFactory, log);Console.WriteLine(box1.Product.Name);Console.WriteLine(box2.Product.Name);}}interface IProductFactory{Product Make();}class PizzaFactory : IProductFactory // 这个类实现了IProductFactory的接口{public Product Make(){// 重构是指基本不改变原来的代码,只是把代码放到更合适的地方去Product product = new Product();product.Name = "Pizza";product.Price = 20;return product;}}class ToyFactory : IProductFactory{public Product Make(){Product product = new Product();product.Name = "Toy Cat";product.Price = 120;return product;}}class Logger{public void Log(Product product){// Log 以回调的形式传进模版的方法里Console.WriteLine("Product '{0}' created at {1}. Price is {2}.", product.Name, DateTime.UtcNow, product.Price);}}class Product{public string Name { get; set; }public double Price { get; set; }}class Box{public Product Product { get; set; }}class WrapFactory{public Box WrapProduct(IProductFactory productFactory, Action<Product> logCallback){// 模板方法Box box = new Box();Product product = productFactory.Make(); if (product.Price >= 50){logCallback(product);}box.Product = product;return box;}}
}

可以看到,重构之后,使用接口之后,程序没有委托的身影,也就没有方法级别的耦合;
这个例子说明可以使用接口取代委托;


Section 2 事件

Part 1 初步了解事件

  • 定义:Event,译为“事件”
    • 能够发生的东西,特别是一些比较重要的;
    • a thing that happens, especially something important.
    • 通顺的解释就是“能够发生的什么事情”,叫做事件;
  • 角色:使对象或类具备通知能力的成员
    • 在 C# 语言中,事件是一种类型的成员,是一种使对象或类能够提供通知的成员
    • An event is a member that enables an object or class to provide notifications.
    • 对象 A 拥有一个时间 B的意思是:当B发生的时候,A有能力通知别的对象;
    • 经由事件发送出来的,与事件本身相关的消息,称为 事件参数 EventArgs
    • 根据同时和事件参数来采取行动的行为,称为响应时间或处理事件,处理事件时所做的事情就叫做事件处理器 Event Handler.
  • 使用:用于对象或类之间的动作协调与信息传递(消息推送)
    • 事件的功能 = 通知别的对象或者类 + 可选的事件参数(即详细信息)
  • 原理:事件模型(event model)(也叫做发生-响应模型)中的两个 “5”
    • “发生 -> 响应”中的五个部分:闹钟响了你起床、孩子饿了你做饭…这里面隐含着“订阅”的关系;
    • “发生 -> 响应”中的五个动作:
      • (1)我有一个事件;
      • (2)一个人或一群人关心我的这个事件;
      • (3)我的这个事件发生了;
      • (4)关心这个事件的人会被一次通知到;
      • (5)被通知到的人根据拿到的事件信息(又称“时间数据”、“事件参数”、“通知”)对事件进行相应(又称“处理事件”);
  • 需要规定一下相关的术语以便于交流和学习
    • 事件的订阅者,与事件消息的接收者、时间的响应者、事件的处理者、被事件所通知的对象是一样的,便于交流,只用事件的订阅者
    • 事件参数,与事件信息、事件消息、事件数据是一样的,便于交流,只使用事件参数
  • 提示
    • 事件多用于桌面、手机等开发的客户端编程,因为这些客户端程序经常是用户通过事件来“驱动”的;
    • 事件模型是从现实世界抽象而来的,各种编程语言对这个机制的实现方法不尽相同;
    • Java 语言里没有事件这种成员,也没有委托这种数据类型,Java的事件是使用接口来实现的;
    • 事件模式是好东西,但是是有缺陷的,如果编写的时候没有约束,程序的逻辑容易混乱,经过长期的总结下来,总结出MVC,MVP,MVVM等架构模式,这些是事件模式更高级、更有效的用法;
    • 日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少;

Part 2 事件的应用

  • 事件模型的五个组成部分
    • 事件的拥有者 event source,对象,事件是被拥有者的内部触发的;
    • 事件成员,也就是事件本身,event,成员
    • 事件的响应者,也就是事件的订阅者 event subscriber,对象,当事件发生的时候,有哪些对象被通知到了,就是事件响应者;
    • 事件的处理器 event handler,成员,本质上是一个回调方法
    • 事件的订阅,把事件处理器与事件关联在一起,本质上是一种以委托类型为基础的“约定”
  • 注意
    • 事件处理器是方法成员
    • 挂接事件处理器的时候,可以使用委托实例,也可以直接使用方法名,这是个语法糖;
    • 事件处理器对事件的订阅不是随意地,匹配是否声明事件时所使用的委托类型来检测;
    • 事件可以同步调用也可以异步调用;

下面是一个小例子

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){System.Timers.Timer timer = new System.Timers.Timer(); // 事件拥有者 timertimer.Interval = 1000; // msBoy boy = new Boy(); // 事件的响应者是 boy 对象Girl girl = new Girl();timer.Elapsed += boy.Action;// += 是订阅的写法,后面要跟上事件响应者的事件处理器timer.Elapsed += girl.Action;// 事件 Elapsed,事件订阅 += timer.Start();Console.ReadLine();}}class Boy{// 事件的处理器internal void Action(object sender, ElapsedEventArgs e){Console.WriteLine("Jump!");}}class Girl{internal void Action(object sender, ElapsedEventArgs e){Console.WriteLine("Sing!");}}
}

上面展示的是一个事件同时有两个事件处理器的时候的样子;


在这里插入图片描述
上图展示的是标准的事件机制模型,结构清晰,是MVC、MVP等设计模式的雏形;
下面的程序是对这个标准的事件机制模型的解释

namespace WindowsFormsApp1
{internal static class Program{/// <summary>/// The main entry point for the application./// </summary>[STAThread]static void Main(){Form form = new Form(); // 事件的拥有者 formController controller = new Controller(form); // 事件的响应者 controllerform.ShowDialog();}}class Controller{private Form form;public Controller(Form form){if (form != null){this.form = form;this.form.Click += this.FormClicked; // 事件是 form 的 click,+=实现事件订阅}}// 事件处理器private void FormClicked(object sender, EventArgs e){this.form.Text = DateTime.Now.ToString();}}
}

在这里插入图片描述


在这里插入图片描述
上图展示的是对象用自己的方法订阅、处理自己的事件;
下面的程序是对上图的解释,同时接触到了什么是派生

namespace WindowsFormsApp1
{internal static class Program{/// <summary>/// The main entry point for the application./// </summary>[STAThread]static void Main(){MyForm form = new MyForm(); // 事件的拥有者 form,事件的响应者也是 fromform.Click += form.FormClicked; // 事件是 Click,事件的订阅是 +=form.ShowDialog();}}class MyForm : Form // 派生,集成原有的方法之外还可以添加新的方法{// 事件处理器internal void FormClicked(object sender, EventArgs e){this.Text = DateTime.Now.ToString();}}
}

在这里插入图片描述


在这里插入图片描述
上图展示的是使用最多的,特点是,事件的拥有者是事件的响应者的字段成员,是Windows上默认的事件订阅和处理结构;
下面的程序是对上图示例的解释

namespace WindowsFormsApp1
{internal static class Program{/// <summary>/// The main entry point for the application./// </summary>[STAThread]static void Main(){MyForm form = new MyForm();form.ShowDialog();}}// 事件的响应者是 MyForm 的对象class MyForm : Form{private TextBox textBox;private Button button; // button 是事件的拥有者,且为字段成员public MyForm(){this.textBox = new TextBox();this.button = new Button();// 显示在 form 当中this.Controls.Add(this.textBox);this.Controls.Add(this.button);this.button.Click += this.ButtonClicked; // 事件是 Click// += 是事件的订阅this.button.Text = "Say Hello!";this.button.Top = 100;}// 事件的处理器private void ButtonClicked(object sender, EventArgs e){this.textBox.Text = "Hello World";}}
}

在这里插入图片描述

Part 3 事件的声明

完整的事件声明方式 示例

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Customer customer = new Customer(); // 事件的拥有者Waiter waiter = new Waiter(); // 事件的响应者customer.Order += waiter.Action; // 使用 Action 的方法作为 waiter 类型的事件处理器// Order 事件 += 事件的订阅customer.Action();customer.PayTheBill();}}public class OrderEventArgs : EventArgs{public string DishName { get; set; }public string Size { get; set; }}public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);public class Customer // 需要保证访问级别是一致的{ private OrderEventHandler orderEventHandler;// 事件 Orderpublic event OrderEventHandler Order {add {this.orderEventHandler += value;}remove{this.orderEventHandler -= value;}}public double Bill { get; set; }public void PayTheBill(){Console.WriteLine("I will pay ${0}", this.Bill);}public void WalkIn(){Console.WriteLine("Walk into the restaurant.");}public void SitDown(){Console.WriteLine("Sit Dowm.");}public void Think(){for (int i = 0; i < 5; i++){Console.WriteLine("Let me think.......");Thread.Sleep(1000);}if (this.orderEventHandler != null){ OrderEventArgs e = new OrderEventArgs();e.DishName = "Kongpao Chicken";e.Size = "large";this.orderEventHandler.Invoke(this, e);}}public void Action(){Console.ReadLine();this.WalkIn(); ;this.SitDown();this.Think();}}// 事件的响应者public class Waiter{internal void Action(Customer customer, OrderEventArgs e){Console.WriteLine("I will serve you the dish - {0}", e.DishName);double price = 10;switch (e.Size){case "small":price = price * 0.5;break;case "large":price = price * 1.5;break;default:break;}customer.Bill += price;}}
}

简略的事件声明方式 示例

namespace ConsoleHelloWorld
{class Program{static void Main(string[] args){Customer customer = new Customer(); // 事件的拥有者Waiter waiter = new Waiter(); // 事件的响应者customer.Order += waiter.Action; // 使用 Action 的方法作为 waiter 类型的事件处理器// Order 事件 += 事件的订阅customer.Action();customer.PayTheBill();}}public class OrderEventArgs : EventArgs{public string DishName { get; set; }public string Size { get; set; }}public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);public class Customer // 需要保证访问级别是一致的{public event OrderEventHandler Order;public double Bill { get; set; }public void PayTheBill(){Console.WriteLine("I will pay ${0}", this.Bill);}public void WalkIn(){Console.WriteLine("Walk into the restaurant.");}public void SitDown(){Console.WriteLine("Sit Dowm.");}public void Think(){for (int i = 0; i < 5; i++){Console.WriteLine("Let me think.......");Thread.Sleep(1000);}if (this.Order != null){ OrderEventArgs e = new OrderEventArgs();e.DishName = "Kongpao Chicken";e.Size = "large";this.Order.Invoke(this, e);}}public void Action(){Console.ReadLine();this.WalkIn(); ;this.SitDown();this.Think();}}// 事件的响应者public class Waiter{internal void Action(Customer customer, OrderEventArgs e){Console.WriteLine("I will serve you the dish - {0}", e.DishName);double price = 10;switch (e.Size){case "small":price = price * 0.5;break;case "large":price = price * 1.5;break;default:break;}customer.Bill += price;}}
}

为什么有了委托类型的字段,还需要事件?

  • 事件成员可以让程序的逻辑和对象之间的关系变得更加有道理、安全;

Part 4 澄清

在这里插入图片描述


Section 3 类

面向对象编程,有三个主要特征:封装、继承和多态;
什么是类

  • 是一种数据结构 data structure
  • 是一种数据类型
namespace ConsoleHelloWorld {class Program {static void Main(string[] args) {Student student = new Student(id: 1, name: "Tim"); // 创建实例student.Report();}}class Student {public Student(int id, string name) {this.ID = id; ;this.Name = name;}// 析构器,手动释放资源// 当程序关闭之前,变量 student 没人引用,系统会自动调用析构器~ Student() {Console.WriteLine("Bye bye, Release the system resources");}public int ID { get; set; }public String Name { get; set; }public void Report() {Console.WriteLine($"I'm #{this.ID} student, my name is {this.Name}.");}}
}

Part 1 类的声明

  • 类可以声明在以下三个位置:
    • 名称空间之内(最常见的)
    • 显式名称空间之外(实际上是声明在全局名称空间里面)
    • 类体里面(成员类,或者叫做嵌套类,用得也比较多)

声明 Declaration,定义是 Definition,在 C#和Java中可以混用理解,但在 C++ 当中这二者不是一个东西。在 C++ 中类的声明与类的定义是分开的,C++ 如果要应用类是需要引用头文件,而在 C# 和 Java 当中是不需要的;
因此在 C# 语言中,声明即定义
在这里插入图片描述
类命名中的 class 类名 和 类体 是不可缺少的

  • 类修饰符是可选的:

    • new
    • public
    • protected
    • internal
    • private
    • abstract
    • sealed
    • static
  • new 修饰符适用于嵌套类,它指定类隐藏同名的继承成员,如果在不是嵌套类声明的类声明中使用 new 修饰符,则会导致编译时的错误;

  • public、protected、internal 和 private 修饰符将控制类的可访问性,根据类声明所处的上下文,这其中的一些修饰符可能不允许使用;

  • abstract、sealed 和 static 是有关继承的;

  • public class 其他的项目是可以访问的,也就是其他的 Assembly 可以访问这个 Assembly 中的类;

  • internal class 是可以在同一个项目中自由访问的,也就是把访问级别限制在项目,也就是 Assembly 当中;

  • private class 仅当这个class是其他类的成员的时候,才能见到

Part 2 类的继承

对于面向对象编程非常重要,其最显著的特征是封装、继承和多态,而多态是基于继承的;

声明继承类的语法是比较简单的:

class + 类名 : 类基础 + 类体

一般来说,基类和派生类是一对,父类和子类是一对;

namespace ConsoleHelloWorld {class Program {static void Main(string[] args) {Type type = typeof(Car);Type baseType = type.BaseType;Console.WriteLine(baseType.FullName);// 可以看到输出 >> ConsoleHelloWorld.Vehicle// 说明 Car 的基类是 Vehicle// "是一个 is a" 概念:// 一个派生类的实例,也是基类的实例,但反过来不正确// 也就是 Car 的实例也是 Vehicle 的实例// 也就是:一个汽车,也是一个交通工具Car car = new Car(); // car 是实例,引用着 new 操作符创建的实例Console.WriteLine(car is Vehicle);// 可以看到输出 >> TrueVehicle vehicle = new Vehicle();Console.WriteLine(vehicle is Car);// 可以看到输出 >> False// 从 "是一个 is a" 概念,引出 "可以用一个父类类型的变量,引用子类类型的实例"// 在多态中是非常有用的,下面是引出概念的示例:Vehicle vehicle1 = new Car();Object o1 = new Vehicle();Object o2 = new Car();}}// 作为基类// 如果声明一个类没有说基类是谁// 下面的例子编译器等效 class Vehicle : Object { }class Vehicle { }// 由 Vehicle 派生而来,Car 继承了 Vehicleclass Car : Vehicle { }
}

需要注意:
如果使用 sealed 来修饰类,那么这个类就不能作为基类来使用
C# 中一个类最多只允许有一个基类,但可以有多个基接口

  • 规范一下用语:
    • 继承/派生自某个基类;
    • 某个类实现了某个基接口;

子类的访问级别不能超过父类的访问级别

Part 3 成员的继承与访问

继承的本质:是派生类在基类已有的成员上,进行横向和纵向的扩展

  • 派生类基于基类目前已有的成员:当继承发生的时候,子类对父类的成员是全盘获得的,父类里有什么成员,子类就全部获得;
  • 派生与继承的过程当中,进行的是扩展:类成员只可能是越来越多,不可能是越来越少;
  • 上述两点导致了:一个类成员,一旦被引入到继承链当中,就会一直向下传递,不可能被从继承链中移除掉;这个是静态语言的特点;
  • 类成员在数量上的扩充就是横向扩展;
  • 不增加类成员的个数,而是对某个类成员的版本进行扩充,就是纵向扩展,也就是重写 override;

例子1:继承链

namespace ConsoleHelloWorld {class Program {static void Main(string[] args) {RaceCar raceCar = new RaceCar();string owner = raceCar.Owner; // Owner 声明在 Vehicle 当中,在继承链中继承下来了,这个 Owner 是去不掉的}}// 基类class Vehicle {public string Owner { get; set; }}class Car : Vehicle { }class RaceCar : Car { }
}

例子2:什么是基类对象,以及如何通过基类对象对基类的类成员进行访问

namespace ConsoleHelloWorld {class Program {static void Main(string[] args) {Car car = new Car();// 继承链上的类,当我们创建实例的时候,// 先从基类的构造器开始,先构造基类对象,再一级一级构造// 最终构造创建的子类对象car.ShowOwner();}}// 基类class Vehicle {// 实例构造器public Vehicle(string owner){this.Owner = owner;}public string Owner { get; set; }}class Car : Vehicle {public Car() : base("N/A") // 调用 Vehicle 构造器的时候传入一个值,或者和 Vehicle 构造器的参数列表一致{this.Owner = "Car Owner";}public void ShowOwner() {Console.WriteLine($"this class owner value: {this.Owner}"); // this. 子类对象上的值Console.WriteLine($"base class owner value: {base.Owner}"); // base. 基类对象上的值,base. 只能向上访问一层// 在这个例子中,二者的值是一样的,因为 Car 继承下来了父类的全部成员,// 所以 this.Owner 和 base.Owner 指向的使内存中同一个值}}class RaceCar : Car { }
}

类成员的访问级别,是以类的访问级别为上限

Part 4 面向对象的实现风格

Class-based 基于类的封装继承多态;
Prototype-based 基于原型的封装继承多态;

Part 5 重写与多态

子类在继承父类之后,并没有在成员的个数上的增加,而是重写了父类的成员的方法;

重写需要在父类的成员上标注 virtual,并在子类的成员上标注 override
如果父类不添加 virtual,子类不添加 override,叫做子类对父类成员的隐藏

类成员的隐藏这种用法不常见;
重写与隐藏的发生条件:函数成员,可见(需要对子类可见,也就是 public 或 protected 的时候才算是可见的),签名一致

public 是不仅对子类可见,且对其他类可见;
protected 是只对子类可见,对其他类不可见;

多态 Polymorphism:当我们用父类变量引用子类实例的时候,调用被重写的成员的时候,总是能够调用到继承类上最新的成员;当调用方法成员的时候,能调用到的,永远是跟实例类型相关的最新的版本;

多态是基于重写机制的(virtual -> override)

下面是有关重写与多态的一个例子:

using System;namespace HelloRider 
{class Program {static void Main(string[] args){var car = new Car();car.Run();// 可以看到输出为 >> Car is running!var vehicle = new Vehicle();vehicle.Run();// 可以看到输出为 >> I'm running!var vehicle2 = new RaseCar();vehicle2.Run();// 可以看到输出为 >> RaceCar is running!// 涉及到多态Vehicle v = new Car();v.Run(); // 因为引用的是 Car 类型的实例// 可以看到输出为 >> Car is running!v.Walking(); // 可以看到输出为 >> I'm walking!// 因为 Car 类型的 Walking 和 Vehicle 类型的 Walking 并没有重写关系// 可以视为这个 Walking 中有两个方法,一个属于 Vehicle 一个属于 Car}}class Vehicle{// 重写需要在父类的成员上标注 virtual,并在子类的成员上标注 overridepublic virtual void Run(){Console.WriteLine("I'm running!");}public void Walking(){Console.WriteLine("I'm walking!");}}class Car : Vehicle{// 子类中的方法在父类中也有,并没有新增方法,只是更新了父类方法的版本// 子类对父类成员的重写,也叫做成员的纵向扩展public override void Run(){Console.WriteLine("Car is running!");}// 如果父类不添加 virtual,子类不添加 override,叫做子类对父类成员的隐藏public void Walking(){Console.WriteLine("Car is walking");}}class RaseCar : Car{// 继承链的重写public override void Run(){Console.WriteLine("RaceCar is running!");}}
}

下面这个例子是对属性进行的重写

using System;namespace HelloRider 
{class Program {static void Main(string[] args){Vehicle vehicle = new Car();vehicle.Run();Console.WriteLine(vehicle.Speed);}}class Vehicle{private int _speed;public virtual int Speed{get { return _speed;}set { this._speed = value; }}// 重写需要在父类的成员上标注 virtual,并在子类的成员上标注 overridepublic virtual void Run(){Console.WriteLine("I'm running!");this._speed = 100;}public void Walking(){Console.WriteLine("I'm walking!");}}class Car : Vehicle{private int _rpm;public override int Speed{get{return this._rpm / 100;}set{this._rpm = value / 100;}}// 子类中的方法在父类中也有,并没有新增方法,只是更新了父类方法的版本// 子类对父类成员的重写,也叫做成员的纵向扩展public override void Run(){Console.WriteLine("Car is running!");this._rpm = 5000;}// 如果父类不添加 virtual,子类不添加 override,叫做子类对父类成员的隐藏public void Walking(){Console.WriteLine("Car is walking");}}class RaseCar : Car{// 继承链的重写public override void Run(){Console.WriteLine("RaceCar is running!");}}
}

Section 4 类的扩展知识

接口与抽象类是面向对象设计中最重要的部分,是软件工业设计的两块基石;
SOLID 原则:单一职责原则、开放关闭原则、里氏替换原则、接口隔离原则和依赖倒置原则;

Part 1 抽象类

下面的代码解释了什么是抽象类以及开放关闭原则的简单释义

namespace HelloRider 
{class Program {static void Main(string[] args){}}// 抽象类:指的是函数成员没有被完全实现的类abstract class Student{abstract public void Study(); // 没有被实现的函数成员一定用 abstract 关键字修饰,且不能用 private// 被 abstract 修饰的方法,只有返回值,方法名和参数列表,没有方法体,是完全没有被实现的方法,是抽象方法// 一旦一个类里有抽象方法或其他抽象成员,这个类就变成了抽象类,抽象类前面必须加上 abstract// 因为抽象类含有未被实现的成员,因此编译器不允许实例化这个抽象类// 两个作用:// 1、作为基类,让别人从自己派生出去,在派生类实现没有实现的方法;// 2、作为基类,声明变量,用基类类型的变量去引用子类类型的实例;}}/** 为做基类而生的“抽象类”与“开放/关闭原则”* * 开闭原则:我们应该封装一些不变的、稳定的、固定的和确定的成员,* 而把那些不确定的、有可能改变的成员声明为抽象成员,并留给子类去实现** */

下面的例子是:为做基类而生的“抽象类”与“开放/关闭原则”:

using System;namespace HelloRider 
{class Program {static void Main(string[] args){Vehicle v = new RaceCar(); // 抽象类的唯一能做的事情就是给别的类当基类,并且引用一些已经完全实现抽象成员的子类实例v.Run();}}// 程序既有抽象类,也遵守开闭原则abstract class Vehicle{public void Stop(){Console.WriteLine("Stopped!");}public void Fill(){Console.WriteLine("Pay and fill......");}public abstract void Run();}class Car : Vehicle{public override void Run() // 实现抽象方法的时候也需要加上 override{Console.WriteLine("Car is running......");}}class Truck : Vehicle{public override void Run(){Console.WriteLine("Truck is running......");}}class RaceCar : Vehicle{public override void Run(){Console.WriteLine("Race Car is running......");}}
}

纯抽象类:

using System;namespace HelloRider 
{class Program {static void Main(string[] args){Vehicle v = new RaceCar(); // 抽象类的唯一能做的事情就是给别的类当基类,并且引用一些已经完全实现抽象成员的子类实例v.Run();}}// 纯抽象类,在 C# 中实际上就是接口abstract class VehicleBase{abstract public void Stop();abstract public void Fill();abstract public void Run();}// 程序既有抽象类,也遵守开闭原则abstract class Vehicle : VehicleBase{public override void Stop(){Console.WriteLine("Stopped!");}public override void Fill(){Console.WriteLine("Pay and fill......");}}class Car : Vehicle{public override void Run() // 实现抽象方法的时候也需要加上 override{Console.WriteLine("Car is running......");}}class Truck : Vehicle{public override void Run(){Console.WriteLine("Truck is running......");}}class RaceCar : Vehicle{public override void Run(){Console.WriteLine("Race Car is running......");}}
}

纯抽象类在 C# 中就是接口,下面是示例

using System;namespace HelloRider 
{class Program {static void Main(string[] args){Vehicle v = new RaceCar(); // 抽象类的唯一能做的事情就是给别的类当基类,并且引用一些已经完全实现抽象成员的子类实例v.Run();}}// 纯抽象类,在 C# 中实际上就是接口interface IVehicle{void Stop();void Fill();void Run();}// 类实现接口// 通过 抽象类作为不完全的实现,将其作为基类再创建具体类abstract class Vehicle : IVehicle{public  void Stop(){Console.WriteLine("Stopped!");}public  void Fill(){Console.WriteLine("Pay and fill......");}abstract public void Run();}class Car : Vehicle{public override void Run() // 实现抽象方法的时候也需要加上 override{Console.WriteLine("Car is running......");}}class Truck : Vehicle{public override void Run(){Console.WriteLine("Truck is running......");}}class RaceCar : Vehicle{public override void Run(){Console.WriteLine("Race Car is running......");}}
}
  • 什么是接口和抽象类
    • 接口和抽象类都是软件工程的产物,如果不遵循软件工程,将提高代码的维护难度;
    • 具体类 -> 抽象类 -> 接口:越来越抽象,内部实现的东西越来越少;
    • 抽象类是未完全实现逻辑的类(可以由字段和非public成员,它们代表了具体逻辑);
    • 抽象类为复用而生:专门作为基类来使用,也具有解耦功能;
    • 封装确定的,开放不确定的,推迟到合适的子类中去实现;
    • 接口是完全未实现逻辑的类,纯虚类,只有函数成员,所有成员都是 public,也是隐式 public;
    • 接口为解耦而生:高内聚,低耦合,方便单元测试;
    • 接口是一个协约,早已为工业生产所熟知;
    • 它们都不能实例化,只能用来声明变量、引用具体类(concrete class)的实例;

Part 2 接口

接口:由抽象类进化而来;接口中的成员方法必须是 public,因此写的时候,不需要明写public,默认的就是public;
接口的本质:服务的调用者与服务的提供者之间的契约,对双方必须是可见的,因此使用 public;
接口与类的关系
在抽象世界中,是类与类、对象与对象之间的分工与合作,这个合作叫做依赖,依赖的同时就出现了耦合,依赖越直接,耦合就越紧,下面就是解释依赖与耦合的例子:

namespace HelloRider 
{class Program {static void Main(string[] args){var engine = new Engine();var car = new Car(engine);car.Run(3);Console.WriteLine(car.Speed);// 可以看到输出结果为 >> 30}}class Engine{public int RPM { get; private set; }public void Work(int gas){this.RPM = 1000 * gas;}}class Car{// 此时 Car 和 Engine 类型紧耦合在一起,Car 依赖在 Engine 上// 紧耦合的问题在于,基础的类如果出问题,会导致依赖这个类的类出现问题;// 紧耦合会导致调试难度升高,这就是紧耦合带来的弊端;private Engine _engine;public Car(Engine engine){_engine = engine;}public int Speed { get; private set; }public void Run(int gas){_engine.Work(gas);this.Speed = _engine.RPM / 100;}}
}

如何解决紧耦合的问题?那就是引入接口,下面是例子:

namespace HelloRider 
{class Program {static void Main(string[] args){var user = new PhoneUser(new EricssonPhone()); // 只在这块换了类名,而 User class 还有其他的代码都没变动// 引入接口之后,耦合变得非常的松,松耦合user.UsePhone();}}class PhoneUser{private IPhone _phone;public PhoneUser(IPhone phone){_phone = phone;}public void UsePhone(){_phone.Dail();_phone.PickUp();_phone.Send();_phone.Receive();}}interface IPhone{void Dail();void PickUp();void Send();void Receive();}class NokiaPhone : IPhone{public void Dail(){Console.WriteLine("Nokia calling ...");}public void PickUp(){Console.WriteLine("Hello! This is Tim!");}public void Send(){Console.WriteLine("Nokia message ring ...");}public void Receive(){Console.WriteLine("Hello!");}}class EricssonPhone : IPhone{public void Dail(){Console.WriteLine("Ericsson calling ...");}public void PickUp(){Console.WriteLine("Hello! This is Jack!");}public void Send(){Console.WriteLine("Ericsson ring ...");}public void Receive(){Console.WriteLine("Good morning!");}}
}

接口是为了松耦合而生、是为了解耦合而生

Part 3 单元测试与依赖倒置原则

单元测试是依赖倒置原则在开发中的直接应用;
依赖倒置原则:高层模块不应该依赖底层模块,二者都应依赖其抽象;抽象(接口)不应该依赖于细节(具体功能的实现),细节应依赖抽象(按接口格式写实现的程序);
也就是应针对接口编程,不应针对实现编程

namespace HelloRider 
{class Program {static void Main(string[] args){var fan = new DeskFan(new PowerSupply());Console.WriteLine(fan.Work());}}class PowerSupply{public int GetPower(){return 210;}}class DeskFan{private PowerSupply _powerSupply;public DeskFan(PowerSupply powerSupply){_powerSupply = powerSupply;}public string Work(){int power = _powerSupply.GetPower();if (power <= 0){return "Won't work.";}else if (power < 100){return "Slow";}else if (power < 200){return "Work fine";}else{return "Warning!";}}}
}

可以看到上面的例子是一个紧耦合的例子,当我们想要修改电压 power 的值的时候,需要修改 PowerSupply 里面的值。一个类设计好之后,是不能够再去直接动类的代码;为了解决这个紧耦合的问题,引入接口进行解耦,并使用单元测试来测试DeskFan;下面是修改之后的代码;

namespace HelloRider 
{class Program {static void Main(string[] args){var fan = new DeskFan(new PowerSupply());Console.WriteLine(fan.Work());}}public interface IPowerSupply{int GetPower();}public class PowerSupply : IPowerSupply{public int GetPower(){return 210;}}public class DeskFan{private IPowerSupply _powerSupply; // 所有的耦合类型都变成接口类型;public DeskFan(IPowerSupply powerSupply){_powerSupply = powerSupply;}public string Work(){int power = _powerSupply.GetPower();if (power <= 0){return "Won't work.";}else if (power < 100){return "Slow";}else if (power < 200){return "Work fine";}else{return "Explode!";}}}
}

下面是单元测试例子:也是接口在单元测试中的应用;

using NUnit.Framework;
using HelloRider;namespace InterfaceExample.Tests
{public class DeskFanTests{[SetUp]public void Setup(){}[Test]public void PowerLowerThanZero_OK() // 测试电压为 0 时的测试结果是否符合预期{var fan = new DeskFan(new PowerSupplyLowerThanZero());var expected = "Won't work.";var actual = fan.Work();Assert.AreEqual(expected, actual);}[Test] // 测试例子必须加上 [Test]public void PowerHigherThan200_Warning() // 测试电压超过 200 时的测试结果是否符合预期{var fan = new DeskFan(new PowerSupplyHigherThan200());var expected = "Warning!";var actual = fan.Work();Assert.AreEqual(expected, actual);}}class PowerSupplyLowerThanZero : IPowerSupply{public int GetPower(){return 0;}}class PowerSupplyHigherThan200 : IPowerSupply{public int GetPower(){return 220;}}
}

在这里插入图片描述
上图展示的是单元测试的运行结果,可以看到左下角部分,一个测试case通过一个测试case没有通过;因此可以在出现问题的测试样例部分打断点调试;

Part 4 接口隔离原则

违反接口隔离原则,带来的后果有两个

例子1:

namespace HelloRider 
{class Program {static void Main(string[] args){var driver = new Driver(new Car());driver.Drive();}}// 当把包含过多功能的接口类型,传给功能调用者的时候// 必然有一部分功能是用不到的,违反了接口隔离原则// 实现这个接口的类,同时违反了单一职责原则// 针对这个问题的解决方案,把接口拆分为多个小接口,// 本质就是把不同的功能分离开,封装成接口class Driver{private IVehicle _vehicle;public Driver(IVehicle vehicle){_vehicle = vehicle;}public void Drive(){_vehicle.Run();}}interface IVehicle{void Run();}class Car : IVehicle{public void Run(){Console.WriteLine("Car is running");}}class Truck : IVehicle{public void Run(){Console.WriteLine("Truck is running");}}interface ITank{void Fire();void Run();}class LightTank : ITank{public void Fire(){Console.WriteLine("Boom!");}public void Run(){Console.WriteLine("Light Tank is running");}}class MediumTank : ITank{public void Fire(){Console.WriteLine("Boom!!!");}public void Run(){Console.WriteLine("Medium Tank is running");}}class HeavyTank : ITank{public void Fire(){Console.WriteLine("Boom!!!!!!");}public void Run(){Console.WriteLine("Heavy Tank is running");}}
}

如果此时想要传入 ITank 接口,需要修改 Driver 类的代码;这时问题出在把一个胖接口传了进去,有永远用不到的功能 Fire;现在这个设计违反了接口隔离原则,而胖接口是由两个本质不同的东西合并起来的时候,应该把胖接口分裂成两个接口,也就是 Fire 和 Run 分离开;Fire 属于武器类,Run 属于机动车类;下面是修改后的代码

namespace HelloRider 
{class Program {static void Main(string[] args){var driver = new Driver(new Car());var driver = new Driver(new Truck());var driver = new Driver(new HeavyTank());// 可以看到,这个 Driver 只要求能跑,修改完代码后,// 无论是 Car、Truck还是Tank,都可以跑// Driver 只调用 Run,不调用其他无关的,符合了接口隔离原则driver.Drive();}}// 当把包含过多功能的接口类型,传给功能调用者的时候// 必然有一部分功能是用不到的,违反了接口隔离原则// 实现这个接口的类,同时违反了单一职责原则// 针对这个问题的解决方案,把接口拆分为多个小接口,// 本质就是把不同的功能分离开,封装成接口class Driver{private IVehicle _vehicle;public Driver(IVehicle vehicle){_vehicle = vehicle;}public void Drive(){_vehicle.Run();}}interface IVehicle{void Run();}interface IWeapon{void Fire();}class Car : IVehicle{public void Run(){Console.WriteLine("Car is running");}}class Truck : IVehicle{public void Run(){Console.WriteLine("Truck is running");}}interface ITank : IVehicle, IWeapon // 一个接口对多个接口的继承{}class LightTank : ITank{public void Fire(){Console.WriteLine("Boom!");}public void Run(){Console.WriteLine("Light Tank is running");}}class MediumTank : ITank{public void Fire(){Console.WriteLine("Boom!!!");}public void Run(){Console.WriteLine("Medium Tank is running");}}class HeavyTank : ITank{public void Fire(){Console.WriteLine("Boom!!!!!!");}public void Run(){Console.WriteLine("Heavy Tank is running");}}
}

过于追求接口隔离原则和单一职责原则的时候,会产生很多很细碎的、只有一个方法的接口和类,因此一定要掌握平衡,把接口和类的大小要维持平衡。

例子2:传给调用者的胖接口,本身是由两个原本设计很好的小接口合并而来,本来应该传进一个小接口,结果传进了合并了小接口的大接口进来;把原本合格的服务提供者挡在外面;下面是例子和修改建议

namespace HelloRider 
{class Program {static void Main(string[] args){int[] nums1 = {1, 2, 3, 4, 5};ArrayList nums2 = new ArrayList {1, 2, 3, 4, 5};Console.WriteLine(Sum(nums1));Console.WriteLine(Sum(nums2));var nums3 = new ReadOnlyCollection(nums1);foreach (var n in nums3){Console.WriteLine(n);}Console.WriteLine(Sum(nums3)); // 此时 Sum 函数无法处理 nums3// 虽然现在只用得到迭代,但现在传入的使 ICollection// 把一些合格的 Service Provider 挡在外面// 只需要把 Sum 传入的 ICollection 改为 IEnumerable 即可// 这时符合接口隔离原则:调用者绝不多要用不着的功能// static int Sum(IEnumerable nums)}static int Sum(ICollection nums){int sum = 0;foreach (var num in nums){sum += (int) num;}return sum;}}// 只能用于迭代,不能添加也不能删除的集合class ReadOnlyCollection : IEnumerable{private int[] _array;public ReadOnlyCollection(int[] array){_array = array;}public IEnumerator GetEnumerator(){return new Enumerator(this);}// 成员类public class Enumerator : IEnumerator{private ReadOnlyCollection _collection;private int _head;public Enumerator(ReadOnlyCollection collection){_collection = collection;_head = -1;}public bool MoveNext(){if (++_head < _collection._array.Length){return true;}else{return false;}}public void Reset(){_head = -1;}public object Current{get{object o = _collection._array[_head];return o;}}}}
}

例子3:专门展示 C# 中特有的:显式接口实现

namespace HelloRider 
{class Program {static void Main(string[] args){var wk = new WarmKiller();wk.Love();wk.Kill();// 此时,Love 和 Kill 方法都轻易能被调用var wk2 = new WarmKillerAAA();wk2.Love(); // 此时只能看到 Love 看不到 KillerIKiller killer = new WarmKillerAAA();killer.Kill(); // 此时才能看到 Killer// 如果此时要用 Love 方法var wk3 = killer as WarmKiller; // 强制类型转换wk3.Love(); // 就能看到 Love 方法了}}interface IGentleman{void Love();}interface IKiller{void Kill();}// 普通的接口隔离与普通的类与实现class WarmKiller : IGentleman, IKiller{public void Love(){Console.WriteLine("I will love you for ever ...");}public void Kill(){Console.WriteLine("Let me kill you ...");}}// 普通的接口隔离与普通的类与实现class WarmKillerAAA : IGentleman, IKiller{public void Love(){Console.WriteLine("I will love you for ever ...");}// 只有把这个类的实例,当做 IKiller 类型的实例的时候,这个方法才能被调用// 也就是 只有 Killer 类型的变量来引用 WarmKillerAAA 类,类型的时候,这个方法才能被调用void IKiller.Kill() {Console.WriteLine("Let me kill you ...");}}
}

Part 5 反射 (待补充)

反射不是 C# 语言的功能,而是 .Net 框架的功能。
下面的例子是反射的基本原理

class Program {static void Main(string[] args){ITank tank = new HeavyTank();// ==========================// 分割线以下不再使用静态类型// 完全是从内存中读取出动态的类型的描述,MetaData 和方法的表述// 这块展示的使反射的基本原理var t = tank.GetType();object o = Activator.CreateInstance(t);MethodInfo fireMi = t.GetMethod("Fire");MethodInfo runMi = t.GetMethod("Run");fireMi.Invoke(o, null);runMi.Invoke(o, null);}}class Driver{private IVehicle _vehicle;public Driver(IVehicle vehicle){_vehicle = vehicle;}public void Drive(){_vehicle.Run();}}interface IVehicle{void Run();}interface IWeapon{void Fire();}class Car : IVehicle{public void Run(){Console.WriteLine("Car is running");}}class Truck : IVehicle{public void Run(){Console.WriteLine("Truck is running");}}interface ITank : IVehicle, IWeapon // 一个接口对多个接口的继承{}class LightTank : ITank{public void Fire(){Console.WriteLine("Boom!");}public void Run(){Console.WriteLine("Light Tank is running");}}class MediumTank : ITank{public void Fire(){Console.WriteLine("Boom!!!");}public void Run(){Console.WriteLine("Medium Tank is running");}}class HeavyTank : ITank{public void Fire(){Console.WriteLine("Boom!!!!!!");}public void Run(){Console.WriteLine("Heavy Tank is running");}}

在实际的工作中,大部分接触的都是封装好的反射。
封装好的反射最重要的功能之一是依赖注入 Dependencey Injection,DI;

Part 6 泛型 Generic (待补充)

泛型无处不在,是面向对象编程中与接口相当的位置;
为什么需要泛型?避免成员膨胀或者类型膨胀
正交性:泛型类型,泛型成员
泛型的东西,在编程的时候是不能直接拿来使用的,需要经过特化才能使用;
泛型类凡是经过特化之后,使用到类型参数的地方,都是强类型的
下面的例子是:泛型类既不产生类型膨胀,也不产生成员膨胀

namespace HelloRider 
{class Program {static void Main(string[] args){Apple apple = new Apple() {Color = "Red"};Box<Apple> box1 = new Box<Apple>() {Cargo = apple};Console.WriteLine(box1.Cargo.Color);// 可以看到输出为 >> RedBook book = new Book() {Name = "New Book"};Box<Book> box2 = new Box<Book>() {Cargo = book};Console.WriteLine(box2.Cargo.Name);// 可以看到输出为 >> New Book}}class Apple{public string Color { get; set; }}class Book{public string Name { get; set; }}class Box<TCargo> // 普通类改成泛型类就是在类名后加上尖括号,在尖括号内写上类型参数{public TCargo Cargo { get; set; }}
}

下面的例子是泛型接口的例子:

namespace HelloRider 
{class Program {static void Main(string[] args){Student<int> student = new Student<int>();student.ID = 101;student.Name = "Tim";Teacher teacher = new Teacher();teacher.ID = 1001;teacher.Name = "Cheems";}}interface IUnique<TId>{public TId ID { get; set; }}// 如果一个类实现的是泛型接口,那么这个类本身也是泛型的// 这个类是实现泛型接口,所以成了泛型类 {第一个方法}class Student<TId> : IUnique<TId>{public TId ID { get; set; }public string Name { get; set; }}// 在实现泛型接口的时候,是实现特化之后的泛型接口,那么这个类就不再是泛型类 {第二个方法}class Teacher : IUnique<ulong>{public ulong ID { get; set; }public string Name { get; set; }}
}

泛型之所以对编程的影响很广泛,在 C# 中几乎所有常用的数据结构都是泛型的;

相关文章:

C# 学习笔记--个人学习使用 <2>

C# 学习笔记 Chapter 2 比较硬的基础部分Section 1 委托Part 1 Action 与 func 委托的示例Part 2 自定义委托Part 3 委托的一般使用Part 4 委托的高级使用Part 5 适时地使用接口 Interface 取代一些对委托的使用 Section 2 事件Part 1 初步了解事件Part 2 事件的应用Part 3 事件…...

Linux网络编程Socket通信6-Libevent移植与使用

目录 libeventlibevent交叉编译并移植libevent安装安装步骤测试代码libevent执行报错解决 libevent_base根节点event_base_newevent_base_freeevent_reinit event_loop循环等待事件event_base_loopevent_base_dispatchevent_base_loopexitevent_base_loopbreak event事件event_…...

c#:委托 泛型委托的使用 泛型约束

委托 在 C# 中&#xff0c;delegate 是一种引用类型&#xff0c;它允许您定义和使用可以引用特定方法的对象。delegate 可以看作是一种函数指针&#xff0c;它可以在运行时动态地调用不同的方法。 以下是一个简单的例子来说明 delegate 的实际作用&#xff1a; // 1. 定义一…...

大数据之linux入门

一、linux是什么 linux操作系统 开发者是林纳斯-托瓦兹&#xff0c;出于个人爱好编写。linux是一个基于posix和unix的多用户、多任务、支持多线程和多CPU的操作系统。 Unix是20世纪70年代初出现的一个操作系统&#xff0c;除了作为网络操作系统之外&#xff0c;还可以作为单…...

MPI之MPI_Sendrecv接口以及空进程概念介绍

MPI_Sendrecv函数原型 int MPI_Sendrecv(const void *sendbuf, int sendcount, MPI_Datatype sendtype, int dest, int sendtag,void *recvbuf, int recvcount, MPI_Datatype recvtype, int source, int recvtag, MPI_Comm comm, MPI_Status *status);其中各个参数的含义如下&…...

Revit SDK:PointCurveCreation 创建点来拟合曲线

前言 这个例子通过留个例子来展示如何通过点来拟合曲线或者曲面。 内容 PointsParabola 生成抛物线的核心逻辑&#xff1a; double yctr 0; XYZ xyz null; ReferencePoint rp null; double power 1.2; while (power < 1.5){double xctr 0;double zctr 0;while (…...

嵌入式Linux开发实操(十五):nand flash接口开发

# 前言 flash memory,分NAND和NOR: 如果说nor flash有个特点就是能执行代码,NOR并行接口具有地址和数据总线,spi flash更是主要用于存储代码,SPI(或QSPI)NOR代码可就地执行(XiP),一般系统要求flash闪存提供相对较高的频率和数据缓存的clocking。而nand flash主要用于…...

vue2 组件库之vetur提示

当我们开发完自定义UI组件库后&#xff0c;在项目中使用时&#xff0c;想要达到以下提示效果&#xff0c;组件提示与属性提示&#xff0c;有什么解决方案呢&#xff1a; 事实上&#xff0c;这是vetur的功能&#xff0c;原文如下&#xff1a; Component Data | Vetur If a pac…...

慕课网 Go工程师 第三周 package和gomodules章节

Go包的引入&#xff1a; 包名前面加匿名&#xff0c;只引入但不使用&#xff0c;如果对应包有init函数&#xff0c;会执行init函数&#xff08;初始化操作&#xff09; 包名前面加. 把这个包的结构体和方法导入当前包&#xff0c;慎用&#xff0c;你不知道当前包和被引入的包用…...

【ES6】JavaScript 中的数组方法reduce

reduce() 是一个 JavaScript 中的数组方法&#xff0c;它会对数组的每个元素执行一个提供的 reducer 函数&#xff0c;将其减少到一个单一的值。 这是 reduce() 的基本用法&#xff1a; //(method) Array<number>.reduce(callbackfn: (previousValue: number, currentV…...

数据结构--树4.2(二叉树)

目录 一、二叉树的定义和特点 1、定义 2、特点 二、二叉树的基本形态 1、空二叉树 2、只有一个根结点 3、根结点只有左子树 4、根结点只有右子树 5、根结点既有左子树又有右子树 6、斜树 7、满二叉树 8、满二叉树和完全二叉树 三、二叉树的性质 一、二叉树的定义和…...

详解Numpy(基于jupyter notbook)

详解Numpy&#xff08;基于jupyter notbook&#xff09; 1.创建数组2.数据类型3.数组切片和索引4.Numpy的广播与数组操作5.数组合并与通用函数6.其他通用函数 1.创建数组 #引入numpy包&#xff0c;以后np就代表numpy import numpy as npanp.arange(10,30,2)#10为起点&#xff…...

uniapp实现:点击拨打电话,弹出电话号码列表,可以选择其中一个进行拨打

一、实现效果&#xff1a; 二、代码实现&#xff1a; 在uni-app中&#xff0c;使用uni.showActionSheet方法实现点击拨打电话的功能&#xff0c;并弹出相关的电话列表供用户选择。 当用户选择了其中一个电话后&#xff0c;会触发success回调函数&#xff0c;并通过res.tapInde…...

swc-loader Segmentation fault “$NODE_EXE“ “$NPM_CLI_JS“ “$@“

webpack swc swc还不是很稳定。 在swcrc 中有配置plugins 时&#xff0c;swc 转换 /node_modules/ 会报错。 环境 swc/cor1.3.62swc-loader0.2.3swc-plugin-vue-jsx0.2.5 解决 配两套rule,一套处理项目代码&#xff0c;一套处理node_modules webpack.config.js rules:…...

Leetcode78. 子集

给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 回溯法 class Solution {public List<List<Integer>> subsets(int[] nums) {List…...

百度“AI智障”到AI智能体验之旅

目录 前言一、百度PLATO1.抬杠第一名2.听Ta瞎扯淡3.TA当场去世了4.智障与网友的高光时刻 二、文心一言1.设计测试用例2.随意发问3.手机端约会神器 三、体验总结&#xff1a;四、千帆大模型 前言 最近收到了文心一言3.5大模型的内测资格&#xff0c;正巧之前也体验过它的前身&q…...

R中当并行运算遇到C++函数时,让foreach+Rcpp一起工作

目录 方案一&#xff1a;C函数在R包中 方案二&#xff1a;C函数在本地&#xff0c;通过Rcpp::sourceCpp("fun_name.cpp")使用 方案三&#xff1a;将C函数写在当前脚本中 题外话&#xff1a;为什么要研究foreachRcpp? 本文参考&#xff1a; 问题&#xff1a;在fo…...

实现带头双向循环链表

&#x1f308;带头双向循环链表 描述&#xff1a;一个节点内包含两个指针&#xff0c;一个指向上一个节点&#xff0c;另一个指向下一个节点。哨兵位指向的下一个节点为头节点&#xff0c;哨兵位的上一个指向尾节点。 结构优势&#xff1a;高效率找尾节点&#xff1b;高效率插入…...

Mysql 表字符集变更

背景 线上有几张表的字符集是 latin1&#xff0c;要求换成utf8mb4。至于操作的时机则需要自行判断。 1.查看库中所有字符集为latin1的所有表 SELECTDISTINCTtable_schema,table_name,collation_name,character_set_name,CONCAT(ALTER TABLE , table_schema, ., table_name, …...

golang抓取tcp包的实现

要抓取 TCP 请求的数据包&#xff0c;你可以使用 golang 中的 packet 库和 pcap 库。下面是一种使用这些库来抓取 TCP 数据包的方法&#xff1a; 首先&#xff0c;确保已经安装了 pcap 库&#xff0c;可以使用以下命令来安装&#xff1a; go get -u github.com/google/gopack…...

oauth2.0第2季 分布式认证与授权实现单点登录

一 oauth介绍 1.0 疑问汇总 1.使用jwttoken进行令牌传输&#xff0c;资源服务器在本地怎么验证token&#xff1f; 1.1 oauth的基础内容 1.1.1 oauth是什么 1.1.2 oauth的角色 1.1.3 oauth的认证流程 1.1.4 oauth的4种模式 1.2 为何要用oauth2.0 1.介绍单体架构 使用ses…...

SpringBoot一些困惑及梳理

Spring中常用的classpath前缀到底指向哪里? classpath实际就是和java命令行运行时指定的classpath是同一个概念&#xff0c;在ideamaven中也就是指向target/classes目录。不要被网上哪些复制粘贴的文章所迷惑。classpath: 和 classpath*: 到底什么区别? classpath: 实际就是当…...

PostgreSQL汉字转拼音首字母

PostgreSQL汉字转拼音首字母&#xff0c;最近有个需求要做搜索优化&#xff0c;要求提取汉字首字母识别输入&#xff0c;图方便直接数据库用函数批量转换了&#xff0c;整理了网上的两个方法函数备忘&#xff0c;非原创。 https://blog.qdac.cc/?p1281 https://developer.aliy…...

HBuilderX修改manifest.json设置,解决跨域问题(CORS、Cross-Origin)

搭建一个前台uniapp&#xff0c;后台springboot的开发环境时&#xff0c;遇到了跨域问题。 console提示错误信息&#xff1a; Access to XMLHttpRequest at http://10.0.180.203/api/cms/getAdList?apId1 from origin http://localhost:8080 has been blocked by CORS policy…...

AR地图微信小程序:数字化时代下地图应用的新突破

随着数字化时代的到来&#xff0c;地图应用成为人们日常生活中不可或缺的工具。而随着增强现实&#xff08;AR&#xff09;技术的快速发展&#xff0c;AR地图微信小程序应运而生&#xff0c;为用户提供了一种全新的地图导航体验。本文将深入探讨AR地图微信小程序的专业性和思考…...

成集云 | 抖店客户静默下单催付数据同步钉钉 | 解决方案

源系统成集云目标系统 方案介绍 随着各品牌全渠道铺货&#xff0c;主播在平台上直播时客户下了订单后不能及时付款&#xff0c;第一时间客户收不到提醒&#xff0c;不仅造成了客户付款率下降&#xff0c;更大量消耗了企业的人力成本和经济。而成集云与钉钉深度合作&#xff0…...

C++中的运算符总结(5):按位逻辑运算符

C中的运算符总结&#xff08;5&#xff09;&#xff1a;按位逻辑运算符 9、按位运算符 NOT&#xff08; &#xff5e;&#xff09;、 AND&#xff08; &&#xff09;、 OR&#xff08; |&#xff09;和 XOR&#xff08; ^&#xff09; 逻辑运算符和按位运算符之前的差别在…...

《异常检测——从经典算法到深度学习》22 Kontrast: 通过自监督对比学习识别软件变更中的错误

《异常检测——从经典算法到深度学习》 0 概论1 基于隔离森林的异常检测算法 2 基于LOF的异常检测算法3 基于One-Class SVM的异常检测算法4 基于高斯概率密度异常检测算法5 Opprentice——异常检测经典算法最终篇6 基于重构概率的 VAE 异常检测7 基于条件VAE异常检测8 Donut: …...

大数据风控介绍

众所周知&#xff0c;金融是数据化程度最高的行业之一&#xff0c;也是人工智能和大数据技术重要的应用领域。随着大数据收集、存储、分析和模型技术日益成熟&#xff0c;大数据技术逐渐应用到金融风控的各个环节。个推作为专业的数据智能服务商&#xff0c;拥有海量数据资源&a…...

Linux内核学习(九)—— 虚拟文件系统(基于Linux 2.6内核)

虚拟文件系统&#xff08;VFS&#xff09;作为内核子系统&#xff0c;为用户空间程序提供了文件和文件系统相关的接口。通过虚拟文件系统&#xff0c;程序可以利用标准的 Unix 系统调用对不同的文件系统&#xff08;甚至不同介质上的文件系统&#xff09;进行读写操作。 一、通…...

【模拟】算法实战

文章目录 一、算法原理二、算法实战1. leetcode1576 替换所有的问号2. leetcode495 提莫攻击3. leetcode6 N字形变换4. leetcode38 外观数列5. leetcode1419 数青蛙 三、总结 一、算法原理 模拟就是用计算机来模拟题目中要求的操作&#xff0c;模拟题目通常具有代码量大、操作…...

各个微服务模块之间互相依赖调用的问题

首先是模块之间不能够循环引用&#xff0c;否则会报循环依赖引入的错误。 没有了模块之间的相互依赖&#xff0c;在项目中这两个模块是相互调用的&#xff0c;分别各自定义相应的Feign接口&#xff0c;如下&#xff1a; 最开始写的运行报错的代码如下&#xff1a; FeignCli…...

理论转换实践之keepalived+nginx实现HA

背景&#xff1a; keepalivednginx实现ha是网站和应用服务器常用的方法&#xff0c;之前项目中单独用nginx实现过负载均衡和服务转发&#xff0c;keepalived一直停留在理论节点&#xff0c;加之最近工作编写的一个技术文档用到keepalived&#xff0c;于是便有了下文。 服务组件…...

华为OD七日集训第1期复盘 - 按算法分类,由易到难,循序渐进,玩转OD(文末送书)

目录 一、活动内容如下第1天、逻辑分析第2天、字符串处理第3天、数据结构第4天、双指针第5天、递归回溯第6天、二分查找第7天、贪心算法 && 二叉树 二、可观测性工程1、简介2、主要内容 大家好&#xff0c;我是哪吒。 最近一直在刷华为OD机试的算法题&#xff0c;坚持…...

MPI之持久化通信句柄与非持久化通信句柄

MPI_Isend & MPI_Send 创建临时通信句柄 在前面的文章中举了例子&#xff0c;我们使用MPI_Isend接口发送数据时&#xff0c;有个传出参数request&#xff0c;该参数是创建的通信句柄&#xff0c; 实际上该句柄是一个临时句柄&#xff0c;即只用于一次性发送数据的场景&…...

搭建个人备忘录中心服务memos、轻量级笔记服务

目录 一、源码 二、官网 三、搭建 四、使用 一、源码 GitHub - usememos/memos: A privacy-first, lightweight note-taking service. Easily capture and share your great thoughts. 二、官网 memos - Easily capture and share your great thoughts 三、搭建 docke…...

探究代理技术在网络安全、爬虫与HTTP通信中的多重应用

在当今高度互联的世界中&#xff0c;代理技术在网络安全、爬虫开发以及HTTP通信中扮演着举足轻重的角色。本文将深入探讨Socks5代理、IP代理以及HTTP代理在这些领域中的多重应用&#xff0c;探索其如何为我们创造更安全、高效的网络环境。 1. Socks5代理&#xff1a;构建安全通…...

vue左侧漏斗切换 echart图表动态更新

这个需求是根据点击左侧的箭头部分&#xff0c;右侧图表切换&#xff0c;左侧选中数据高亮&#xff08;图片用的svg&#xff09; 一、效果图 二、vue组件 <template><div class"funnel_wrap"><div class"flex_between"><div class&q…...

Centos7安装ZK-UI管理界面安装|Maven|Git|

一: JDK1.8安装 参考: Centos7卸载|安装JDK1.8|Xshell7批量控制多个终端 二&#xff1a;Maven安装 2.1&#xff1a;下载maven安装包 maven 下载地址&#xff1a;https://mirror.bit.edu.cn/apache/maven/maven-3/ [rootwww ~]# mkdir -p /usr/local/maven [rootwww ~]# …...

C语言日常刷题7

文章目录 题目答案与解析1234567 题目 1、如下程序的运行结果是&#xff08; &#xff09; char c[5]{a, b, \0, c, \0}; printf("%s", c)A: ‘a’ ‘b’ B: ab\0c\0 C: ab c D: ab 2、若有定义&#xff1a; int a[2][3]; &#xff0c;以下选项中对 a 数组元素正确…...

037 - 有关时间和日期的函数方法

文档&#xff1a;MySQL :: MySQL 5.7 Reference Manual :: 12.7 Date and Time Functions​​​​​​ 以下为案例&#xff0c;更多内容可查看文档 返回当前日期&#xff1a; CURDATE() 返回当前时间&#xff1a; CURTIME() 返回当前日期和时间&#xff1a; NOW() 返回年份&a…...

(JAVA)树——tree

...

js判断对象是否为空对象的方法总结

js判断对象是否为空对象的方法总结 方法1&#xff1a;JSON.stringify()方法方法2&#xff1a;for in方法方法3&#xff1a;Object.keys()方法方法4&#xff1a;Object.getOwnPropertyNames()方法方法5&#xff1a;jquery 的 isEmptyObject()方法 在面试或者开发过程中&#xff…...

LeetCode1049. 最后一块石头的重量 II

1049. 最后一块石头的重量 II 文章目录 [1049. 最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/)一、题目二、题解方法一&#xff1a;01背包二维数组算法思路具体实现 方法二&#xff1a;01背包一维数组 一、题目 有一堆石头&#xff0c;用整数数…...

universal robot 机械臂 官方基本教程

https://academy.universal-robots.cn/modules/e-Series-core-track/Chinese/module3/story_html5.html?courseId2166&languageChinese 教程1 控制箱内部 包含&#xff1a; 主机板&#xff0c;SD卡&#xff0c;和安全控制板 安全控制板负责所有控制信息&#xff0c;包括…...

网络常见安全漏洞

引言 随着互联网的迅猛发展&#xff0c;网络安全问题日益严重。在网络世界中&#xff0c;各种常见的安全漏洞给人们的通信和数据安全带来了巨大的威胁。本文将介绍一些常见的网络安全漏洞&#xff0c;并提供一些防范措施。 1. XSS&#xff08;跨站脚本攻击&#xff09; 跨站…...

【JS案例】JS实现图片放大镜功能

JS案例图片放大镜 &#x1f31f;效果展示 &#x1f31f;HTML结构 &#x1f31f;CSS样式 &#x1f31f;实现思路 &#x1f31f;具体实现 1.初始化数据图片 2.获取所需DOM元素 3.初始化页面 初始化缩略图 绑定事件 &#x1f31f;完整代码 &#x1f31f;写在最后 &…...

linux centos7 bash中字符串反向输出

给定一个字符串&#xff0c;如何反向(倒序)输出&#xff1f; 字符串反转的方法&#xff1a;a.对各个字符位置进行循环调换&#xff08;从原字符串左边取出放在新字符串的右边&#xff1b;从原字符串右边取出放在新字符串的左边&#xff09;。b.对各个字符由水平排列转为垂直排…...

c++:QT day1 认识与学习

...

git rebase和merge区别

一、概述 merge和rebase 标题上的两个命令&#xff1a;merge和rebase都是用来合并分支的。 这里不解释rebase命令&#xff0c;以及两个命令的原理&#xff0c;详细解释参考这里。 下面的内容主要说的是两者在实际操作中的区别。 1.1 什么是分支 分支就是便于多人在同一项目…...