设计模式之【访问者模式】,动态双分派的魅力

news2024/10/6 2:23:40

文章目录

  • 一、什么是访问者模式
    • 1、访问者模式的应用场景
    • 2、访问者模式的五大角色
    • 3、访问者模式的优缺点
  • 二、实例
    • 1、访问者模式的通用写法
    • 2、宠物喂食实例
    • 3、KPI考核案例
    • 小总结
  • 三、分派
    • 1、什么是分派
    • 2、静态分派
    • 3、动态分派
    • 4、双分派
    • 5、访问者模式中的伪动态双分派
  • 四、访问者模式在源码中的应用
    • 1、FileVisitor
    • 2、BeanDefinitionVisitor

一、什么是访问者模式

访问者模式(Visitor Pattern)是一种将数据结构与数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。属于行为型模式。

访问者模式被称为最复杂的设计模式,并且使用频率不高,设计模式的作者也评价为:大多数情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。

访问者模式的基本思想是,针对系统中拥有固定类型数量的对象结构(元素),在其内提供一个accept()方法来接收访问者对象的访问。不同的访问者对同一元素的访问内容不同,使得相同的元素集合可以产生不同的数据结构。accept()方法可以接收不同的访问者对象,然后在内部将自己(元素)转发到接收到的访问者对象的visit()方法内。访问者内部对应类型的visit()方法就会得到回调执行,对元素进行操作。也就是通过两次动态分发(第一次是对访问者的分发accept()方法,第二次是对元素的分发visit()方法),才最终将一个具体的元素传递到一个具体的访问者,如此一来,就解耦了数据结构与操作,且数据操作不会改变元素状态。

访问者模式的核心是,解耦数据结构与数据操作,使得对元素的操作具备优秀的扩展性,可以通过扩展不同的数据操作类型(访问者)实现对相同元素集的不同操作。

1、访问者模式的应用场景

访问者模式在生活场景中也是用的非常多,例如每年年底的KPI考核,KPI考核标准是相对稳定的,但是参与KPI考核的员工可能每年都会发生变化,那么员工就是访问者。我们平时去食堂或者餐厅吃饭,餐厅的菜单和就餐方式是相对稳定的,但是去餐厅就餐的人员是每天都在发生变化的,因此就餐人员就是访问者。

当系统 中存在类型数目稳定(固定)的一类数据结构时,可以通过访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。
简言之,就是对集合中的不同类型数据(类型数量稳定)进行多种操作,则使用访问者模式,访问者模式适用以下场景:

  • 数据结构稳定、作用于数据结构的操作经常变化的场景;
  • 需要数据结构与数据操作分离的场景;
  • 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。

2、访问者模式的五大角色

在这里插入图片描述

  • 抽象访问者(Visitor):接口或抽象类,该类给每一个具体元素(Element)的访问冠以一个行为visit()方法,其参数就是具体的元素(Element)对象。理论上来说,Visitor的方法个数与元素(Element)个数是相等的。如果元素(Element)个数经常变动,会导致Visitor的方法也要进行变动,此时,该情形并不适用访问者模式;
  • 具体访问者(ConcreteVisitor):实现对具体元素的操作;
  • 抽象元素(Element):接口或抽象类,定义了一个接受访问者访问的方法accept(),表示所有元素类型都支持被访问者访问;
  • 具体元素(Concrete Element):具体元素类型,提供接受访问者的具体实现。通常的实现都为:visitor.visit(this);
  • 结构对象(ObjectStructure):该类内部维护了元素集合,并提供方法接受访问者对该集合所有元素进行操作。

3、访问者模式的优缺点

优点:

  • 解耦了数据结构与数据操作,使得操作集合可以独立变化;
  • 扩展性好:可以通过扩展访问角色,实现对数据集的不同操作;
  • 元素具体类型并非单一,访问者均可操作;
  • 各角色职责分离,符合单一职责原则。

缺点:

  • 无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则;
  • 具体元素变更困难:具体元素增加属性、删除属性等操作会导致对应的访问者类需要进行相应的修改,尤其当有大量访问者类时,修改范围太大;
  • 违背依赖倒置原则:为了达到“区别对待”,访问者依赖的是具体元素类型,而不是抽象。

二、实例

1、访问者模式的通用写法

// 抽象元素
public interface IElement {
    void accept(IVisitor visitor);
}

// 具体元素
public class ConcreteElementA implements IElement {
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    public String operationA() {
        return this.getClass().getSimpleName();
    }
}

// 具体元素
public class ConcreteElementB implements IElement {
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    public int operationB() {
        return new Random().nextInt(100);
    }
}
// 抽象访问者
public interface IVisitor {

    void visit(ConcreteElementA element);

    void visit(ConcreteElementB element);
}
// 具体访问者
public class ConcreteVisitorA implements IVisitor {

    public void visit(ConcreteElementA element) {
        String result = element.operationA();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }

    public void visit(ConcreteElementB element) {
        int result = element.operationB();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }
}
// 具体访问者
public class ConcreteVisitorB implements IVisitor {

    public void visit(ConcreteElementA element) {
        String result = element.operationA();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }


    public void visit(ConcreteElementB element) {
        int result = element.operationB();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }
}
// 结构对象
public class ObjectStructure {
    private List<IElement> list = new ArrayList<IElement>();

    {
        this.list.add(new ConcreteElementA());
        this.list.add(new ConcreteElementB());
    }

    public void accept(IVisitor visitor) {
        for (IElement element : this.list) {
            element.accept(visitor);
        }
    }
}

2、宠物喂食实例

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
在这里插入图片描述

// 抽象访问者接口
public interface Person {
	void feed(Cat cat);
	void feed(Dog dog);
}
// 创建不同的具体访问者角色(主人和其他人),都需要实现 Person 接口
public class Owner implements Person {
	@Override
	public void feed(Cat cat) {
		System.out.println("主人喂食猫");
	}
	@Override
	public void feed(Dog dog) {
		System.out.println("主人喂食狗");
	}
}
public class Someone implements Person {
	@Override
	public void feed(Cat cat) {
		System.out.println("其他人喂食猫");
	}
	@Override
	public void feed(Dog dog) {
		System.out.println("其他人喂食狗");
	}
}
// 抽象节点 -- 宠物
public interface Animal {
	void accept(Person person);
}
// 实现 Animal 接口的 具体节点(元素)
public class Dog implements Animal {
	@Override
	public void accept(Person person) {
		person.feed(this);
		System.out.println("好好吃,汪汪汪!!!");
	}
}
public class Cat implements Animal {
	@Override
	public void accept(Person person) {
		person.feed(this);
		System.out.println("好好吃,喵喵喵!!!");
	}
}
// 定义对象结构,此案例中就是主人的家
public class Home {
	private List<Animal> nodeList = new ArrayList<Animal>();
	public void action(Person person) {
		for (Animal node : nodeList) {
			node.accept(person);
		}
	}
	//添加操作
	public void add(Animal animal) {
		nodeList.add(animal);
	}
}
// 测试类
public class Client {
	public static void main(String[] args) {
		Home home = new Home();
		home.add(new Dog());
		home.add(new Cat());
		Owner owner = new Owner();
		home.action(owner);
		Someone someone = new Someone();
		home.action(someone);
	}
}

3、KPI考核案例

每到年底,管理层就要开始评定员工一年的工作绩效,员工分为工程师和产品经理;管理层有CEO(首席执行官)和CTO(首席技术官)。CTO关注工程师的代码量、产品经理的产品数量;CEO关注KPI。

由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。我们使用访问者模式进行实现。

// 员工抽象类
public abstract class Employee {
    public String name;
    public int kpi;  //员工KPI

    public Employee(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }

    //接收访问者的访问
    public abstract void accept(IVisitor visitor);
}

Employee类定义了员工基本信息及一个accept()方法,该方法表示接受访问者的访问,由具体的子类来实现。访问者是个接口,传入不同的实现类,可以访问不同的数据。

// 工程师
public class Engineer extends Employee {
    public Engineer(String name) {
        super(name);
    }

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    //考核指标是每年的代码量
    public int getCodeLines(){
        return new Random().nextInt(10* 10000);
    }
}
// 产品经理
public class Manager extends Employee {
    public Manager(String name) {
        super(name);
    }

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    //考核的是每年新产品研发数量
    public int getProducts(){
        return new Random().nextInt(10);
    }
}

工程师考核代码梳理,产品经理考核产品数量,二者的职责不同。也正是因为有这样的差异性,才使得访问者模式能够在这个场景下发挥作用。Employee、Engineer、Manager这三个类型就相当于数据结构,这些类型相对稳定,不会发生变化。

将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的showReport方法查看所有员工的业绩:

public class BusinessReport {
    private List<Employee> employees = new LinkedList<Employee>();

    public BusinessReport() {
        employees.add(new Manager("产品经理A"));
        employees.add(new Engineer("程序员A"));
        employees.add(new Engineer("程序员B"));
        employees.add(new Engineer("程序员C"));
        employees.add(new Manager("产品经理B"));
        employees.add(new Engineer("程序员D"));
    }

    public void showReport(IVisitor visitor){
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
    }
}

// 访问者接口,通常由重载方法完成,有几种元素就有几个重载方法
public interface IVisitor {

    void visit(Engineer engineer);

    void visit(Manager manager);
}
// CEO关注KPI
public class CEOVistitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程师" +  engineer.name + ",KIP:" + engineer.kpi);
    }

    public void visit(Manager manager) {
        System.out.println("经理:" +  manager.name + ",KPI:" + manager.kpi + ",产品数量:" + manager.getProducts());
    }
}
// CTO关注代码行数
public class CTOVistitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程师" +  engineer.name + ",代码行数:" + engineer.getCodeLines());
    }

    public void visit(Manager manager) {
        System.out.println("经理:" +  manager.name + ",产品数量:" + manager.getProducts());
    }
}

// 客户端代码
public class Test {
    public static void main(String[] args) {
        BusinessReport report = new BusinessReport();
        System.out.println("==========CEO看报表===============");
        report.showReport(new CEOVistitor());
        System.out.println("==========CTO看报表===============");
        report.showReport(new CTOVistitor());
    }
}

在上述案例中,Employee扮演了Element角色,Engineer和Manager都是ConcreteElement;CEOVisitor和CTOVisitor都是具体的Visitor对象;BusinessReport就是ObjectStructure。

小总结

访问者模式的最大有点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只需要新实现一个访问者接口的类,从而达到数据与数据操作相分离的效果。如果不使用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用if-else和类型转换,这使得代码难以维护。

我们要根据具体情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,是否需要经常定义新的操作,使用访问者模式是否能优化我们的代码,而不是使我们的代码变得更复杂。

三、分派

1、什么是分派

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派

2、静态分派

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

我们来看这个例子:

public class Main {
    public void test(String string){
        System.out.println("string" + string);
    }
    public void test(Integer integer){
        System.out.println("integer" + integer);
    }

    public static void main(String[] args) {
        String string = "1";
        Integer integer = 1;
        Main main = new Main();
        main.test(integer);// integer1
        main.test(string); // string1
    }
}

调用test方法时,编译器根据参数类型和个数,判断方法的版本。

再来看这个例子:

public class Animal {
}
public class Dog extends Animal {
}
public class Cat extends Animal {
}
public class Execute {
	public void execute(Animal a) {
		System.out.println("Animal");
	}
	public void execute(Dog d) {
		System.out.println("dog");
	}
	public void execute(Cat c) {
		System.out.println("cat");
	}
}
public class Client {
	public static void main(String[] args) {
		Animal a = new Animal();
		Animal a1 = new Dog();
		Animal a2 = new Cat();
		Execute exe = new Execute();
		exe.execute(a); // Animal
		exe.execute(a1); // Animal
		exe.execute(a2); // Animal
	}
}

这个结果可能会出乎很多人意料:重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。判断参数类型和参数个数,找到对应方法,这就是多分派的 概念,因为我们有一个以上的考量标准。所以Java是静态多分派的语言,也就是重载

3、动态分派

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。最典型的应用就是多态。

public class Animal {
	public void execute() {
		System.out.println("Animal");
	}
}
public class Dog extends Animal {
	@Override
	public void execute() {
		System.out.println("dog");
	}
}
public class Cat extends Animal {
	@Override
	public void execute() {
		System.out.println("cat");
	}
}
public class Client {
	public static void main(String[] args) {
		Animal a = new Dog();
		a.execute();
		Animal a1 = new Cat();
		a1.execute();
	}
}

上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。

动态分派判断的方法是在运行时获取到对象的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断一句,所以这就是单分派的概念,此时我们的考量标准只有一个,即变量的实际引用类型。相应的,这说明Java是动态单分派的语言

4、双分派

所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。

public class Animal {
	public void accept(Execute exe) {
		exe.execute(this); // 第二次分派
	}
}
public class Dog extends Animal {
	public void accept(Execute exe) {
		exe.execute(this); // 第二次分派
	}
}
public class Cat extends Animal {
	public void accept(Execute exe) {
		exe.execute(this); // 第二次分派
	}
}
public class Execute {
	public void execute(Animal a) {
		System.out.println("animal");
	}
	public void execute(Dog d) {
		System.out.println("dog");
	}
	public void execute(Cat c) {
		System.out.println("cat");
	}
}
public class Client {
	public static void main(String[] args) {
		Animal a = new Animal();
		Animal d = new Dog();
		Animal c = new Cat();
		Execute exe = new Execute();
		a.accept(exe); // 第一次分派
		d.accept(exe); // 第一次分派
		c.accept(exe); // 第一次分派
	}
}

在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也 将自己this作为参数传递进去,这里就完成了第二次分派 ,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了

5、访问者模式中的伪动态双分派

动态双分派指的是在运行时根据实际对象的类型决定需要调用的方法,并且方法的调用不仅仅取决于参数的类型,还取决于接收者的类型。这种多态的实现方式可以更准确地确定方法执行的上下文,从而提高代码的可读性和灵活性。在使用动态双分派时,通常需要使用反射、接口或者抽象类等机制来实现方法的动态调用。

通过前面的分析,我们知道了Java是静态多分派、动态单分派的语言。Java底层不支持动态的双分派。但是通过使用设计模式,也可以在Java语言里实现伪动态双分派。在访问者模式中使用的就是伪动态双分派,而访问者模式实现的手段就是进行了两次动态单分派来达到这个效果。

在上面的KPI考核案例中,BusinessReport 的showReport方法:

public void showReport(IVisitor visitor){
    for (Employee employee : employees) {
        employee.accept(visitor);
    }
}

这里就是依据Employee和IVisitor两个实际类型决定了showReport方法的执行结果,从而决定了accept方法的动作。

分析accept方法的调用过程:
1、当调用accept方法时,根据Employee的实际类型决定是调用Engineer还是Manager的accept方法。
2、这时accept方法的版本已经确定,假如是Engineer,它的accept方法是调用下面这行代码:

public void accept(IVisitor visitor) {
    visitor.visit(this);
}

此时的this就是Engineer类型,所以对应的是IVisitor接口的visit(Engineer engineer)方法,此时需要再根据访问者的实际类型确定visit()方法的版本,如此一来,就完成了动态双分派的过程。

以上的过程就是通过两次单分派,达到了根据两个实际类型确定一个方法的行为的效果。第一次对accept()方法进行动态分派,第二次对访问者的visit()方法进行动态分派。

注!这里的this的类型并不是动态确定的,你写在哪个类当中,它的静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型,这里一定要搞清楚。

注!动态双分派一定要是两次动态分派合起来才叫动态双分派,动态分派是在运行时发生的,与静态分派有着本质区别,不可以说一次动态分派加一次静态分派就是动态双分派。

四、访问者模式在源码中的应用

1、FileVisitor

我们来看JDK的NIO模块下的FIleVisitor接口,它提供了递归遍历文件树的支持。这个接口上的方法表示了遍历过程中的关键过程,允许你在文件被访问、目录将被访问、目录已被访问、发生错误等等过程上进行控制;换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。

public interface FileVisitor<T> {

    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException;

    FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException;

    FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException;

    FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException;
}

调用FileVisitor中的方法,会返回访问结果FileVisitResult对象值,用于决定当前操作完成后接下来 该如何处理。FileVisitResult的标准返回值存放到FileVisitResult枚举类型中:

  • FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续;
  • FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点;
  • FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点;
  • FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。

通过它去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件,这个类中都提供了相对应的方法。我们来看一下它的实现其实也非常简单:

public class SimpleFileVisitor<T> implements FileVisitor<T> {

    protected SimpleFileVisitor() {
    }

    @Override
    public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(dir);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(file);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(file);
        throw exc;
    }

    @Override
    public FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(dir);
        if (exc != null)
            throw exc;
        return FileVisitResult.CONTINUE;
    }
}

我们来看其基本使用:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;

public class FileVisitorTest {
    public static void main(String[] args) {
        try {
            // 使用FileVisitor对目录进行遍历
            Files.walkFileTree(Paths.get("F:", "\\gvisitor"), new SimpleFileVisitor<Path>() {

                // 在访问子目录前触发该方法

                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问" + dir + "目录");
                    return FileVisitResult.CONTINUE;
                }

                // 在访问文件时触发该方法
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问" + file + "文件");
                    if (file.endsWith("FileVisitorTest.java")) {
                        System.out.println("------FileVisitorTest.java,文件内容-----");
                        List<String> list = Files.readAllLines(file);
                        // 打印出文件的内容
                        System.out.println(list);
                        return FileVisitResult.TERMINATE;
                    }
                    return FileVisitResult.CONTINUE;
                }

                // 在访问失败时触发该方法
                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    // 写一些具体的业务逻辑
                    return super.visitFileFailed(file, exc);
                }

                // 在访问目录之后触发该方法
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    // 写一些具体的业务逻辑
                    return super.postVisitDirectory(dir, exc);
                }
            });
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

2、BeanDefinitionVisitor

Spring IOC中有个BeanDefinitionVisitor类,其中有一个visitBeanDefinition方法,我们看一下源码:

public void visitBeanDefinition(BeanDefinition beanDefinition) {
	visitParentName(beanDefinition);
	visitBeanClassName(beanDefinition);
	visitFactoryBeanName(beanDefinition);
	visitFactoryMethodName(beanDefinition);
	visitScope(beanDefinition);
	if (beanDefinition.hasPropertyValues()) {
		visitPropertyValues(beanDefinition.getPropertyValues());
	}
	if (beanDefinition.hasConstructorArgumentValues()) {
		ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
		visitIndexedArgumentValues(cas.getIndexedArgumentValues());
		visitGenericArgumentValues(cas.getGenericArgumentValues());
	}
}

我们看到该方法中,分别访问了其他数据,比如父类的名字、自己的类名、在IOC容器中的名称等各种信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/539938.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mkv转mp4格式怎么转,5种便捷工具盘点

mkv转mp4格式怎么转&#xff1f;因为当我们下载视频时&#xff0c;通常无法选择格式&#xff0c;这可能会导致下载的视频无法打开。如果下载的是MKV格式&#xff0c;它可以容纳多个音频、视频和字幕流。然而&#xff0c;并非所有播放器都支持MKV格式的视频文件。尽管MKV是常见的…

2D车道线检测算法总结

关于2D车道线检测算法的总结主要分为两类&#xff1a;一类基于语义分割来做&#xff0c;一类基于anchor和关键点来做。还有基于曲线方程来做的&#xff0c;但是落地的话还是上面两种为主。 一、基于语义分割的车道线检测算法 1.LaneNet 论文创新点&#xff1a; 1.将车道线检…

【软考数据库】第十五章 知识产权和标准化

目录 15.1 知识产权概述 15.2 保护期限 15.3 知识产权人的确定 15.4 侵权判断 15.5 标准划分 前言&#xff1a; 笔记来自《文老师软考数据库》教材精讲&#xff0c;精讲视频在b站&#xff0c;某宝都可以找到&#xff0c;个人感觉通俗易懂。 15.1 知识产权概述 知识产权是…

RN_iOS项目部署流程实例

文章目录 1、环境配置1.1 安装node1.2 安装Watchman1.3 安装npm1.4 安装cocoapods 2、百家云demo下载3、运行百家云demo3.1 顺利的话3.2 踩过的坑&#xff08;按这个目录流程走&#xff09;3.2.1 npm install -g react-native-cli3.2.2 安装&#xff1a;npm install3.2.3 npm降…

新一代智能柔性换层跨巷道多车调度的HEGERLS托盘四向穿梭车物流解决方案

随着电子商务和智能制造技术的快速发展&#xff0c;对自动化仓储、密集仓储、自动搬运系统、自动识别、无线通信等多系统集成的需求也在不断增加&#xff0c;物流设备系统的密集化、自动化、智能化等技术也在不断完善。密集存储技术的优势是空间可用性高、运行模式高效、工作人…

Docker-Compose 入门到实战详尽笔记

本文首发自「慕课网」&#xff08;www.imooc.com&#xff09;&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注"慕课网"或慕课网公众号&#xff01; 作者&#xff1a;暮闲 | 慕课网讲师 使用过 Docker 的小伙伴们都知道&#xff0…

职场小白如何在工作中快速的升职加薪

缘起 近来连续两个季度很轻松的获得优秀&#xff0c;在这轻松的背后&#xff0c;一定有些原因支撑这领导给了这个评价。坦白说&#xff0c;最近两个季度&#xff0c;无一天加班&#xff0c;因为我们团队不提倡加班&#xff1b;我这边离领导较远&#xff0c;属于两个城市异地办…

一天吃透Java面试八股文

Java的特点 Java是一门面向对象的编程语言。面向对象和面向过程的区别参考下一个问题。 Java具有平台独立性和移植性。 Java有一句口号&#xff1a;Write once, run anywhere&#xff0c;一次编写、到处运行。这也是Java的魅力所在。而实现这种特性的正是Java虚拟机JVM。已编…

chatgpt赋能Python-pyhton如何安装

Python的安装方法 Python是一种高级编程语言&#xff0c;适用于多种开发需求&#xff0c;从网站构建到机器学习。其易用和灵活的语法使其成为一种非常受欢迎的编程语言。本文将向您介绍如何安装Python。 Python的安装步骤 以下是安装Python的步骤。 步骤1&#xff1a;下载P…

[笔记]初识Burpsuit

文章目录 前言一、安装配置1.1 环境1.2 安装过程1.3 科技过程 二、常用功能2.1 Manual penetration testing features2.2 Advanced/custom automated attacks2.3 Automated scanning for vulnerabilities2.4 Productivity tools2.5 Extensions 三、拓展功能 前言 Burp Suite(b…

【C++】 制作游戏壳

目录 前言 GameFrame游戏壳 搭建游戏壳 游戏初始化 游戏重绘 游戏运行 用回调函数实现游戏运行 关闭窗口&#xff0c;退出程序 测试 增加子类继承游戏壳子 继承 多态 优化 测试 总结 使用方法 常见错误 完整代码 GameFrame.h main.cpp 前言 为了方便以后制…

数据存储梳理记录

目录 1、FMDB-第三方SQLite数据库框架1.1 现状1.2 线程安全问题1.2.1 FMDatabase1.2.2 FMDatabaseQueue1.2.3 FMDatabasePool 2、进程间通信2.1 URL Scheme2.2 keyChain2.3 UIPasteboard2.4 UIDocumentInteractionController2.5 local socket2.6 AirDrop2.7 UIActivityViewCont…

Blender 建模小飞机(基础着色、Cycles渲染引擎)

目录 1. 飞机建模1.1 机身1.2 机身表面细分1.3 机翼1.4 尾翼1.5 尾翼镜像1.6 涡轮1.7 添加经纬球1.8 螺旋桨1.9 螺旋桨调整1.10 柱子1.11 柱子镜像1.12 起落架1.13 轮胎1.14 管1.15 镜像1.16 调整飞机角度 2. 着色 渲染2.1 添加地面2.2 飞机着色2.3 其他材质着色2.4 环境纹理2.…

JSDoc 拥抱 Javascript

JSDoc 在 vs code 已经内置了. 可以在 js 文件的开头添加 // ts-check 即可. 在注释中标注来实现一些 ts 的功能. JSDoc 支持以下注解. Types typeparam (or arg or argument)returns (or return)typedefcallbacktemplate Classes Property Modifiers public, private, p…

Go1.21 速览:Go 终于打算进一步支持 WebAssembly 了。。。

大家好&#xff0c;我是煎鱼。 之前写过一篇关于 Go WebAssembly 的相关文章 《一分钟搞明白&#xff01;快速掌握 Go WebAssembly》&#xff0c;今天带来一则新消息。 想着 Go 过去了那么多年了&#xff0c;只在 Go1.11 支持了 WebAssembly1.0 的部分功能&#xff08;js/sysca…

一个实际音视频开发问题!

前言&#xff1a; 大家好&#xff0c;今天给大家分享的内容是关于平时在做音频编解码会遇到的一些问题&#xff0c;比如说&#xff1a; 解码播放的时候&#xff0c;播不出来解码播放的时候&#xff0c;画面有条纹编码的时候&#xff0c;修改分辨率大小&#xff0c;没有反应 这三…

深圳先进院李骁健团队:植入式脑机接口技术向医疗器械转化的问题与挑战

近几年植入式脑机接口技术取得了非常显著的进步&#xff0c;从工程实现能力和服务功能场景来说&#xff0c;脑机接口技术已经达到了临床应用的临界点&#xff0c;在实验室科研成果向临床医疗器械转化过程中将会面临新的挑战。本文章由此出发&#xff0c;首先介绍了脑机接口技术…

工程监测无线中继采集仪的常用功能与设置

工程监测无线中继采集仪的常用功能与设置 LoRA 频道与中心频率 无线中继采集发送仪使用频道来设置不同的射频中心频率。 中心频率 MHz 基频 (频道) &#xff0c;无线中继采集发送仪 的 LoRA 基频已设置为 420 或者 854MHz&#xff0c;可以使用$STRFxxx 重新设置基频。 例如&a…

RT-Thread 1. GD32移植RT-Thread Nano

1. RT-Thread Nano 下载 RT-Thread Nano 是一个极简版的硬实时内核&#xff0c;它是由 C 语言开发&#xff0c;采用面向对象的编程思维&#xff0c;具有良好的代码风格&#xff0c;是一款可裁剪的、抢占式实时多任务的 RTOS。其内存资源占用极小&#xff0c;功能包括任务处理…

Ansys Zemax | 设计抬头显示器时要使用哪些工具 – 第三部分

本文为使用OpticStudio工具设计优化HUD抬头显示器系统的第三部分&#xff0c;主要包含演示了如何使用OpticStudio非序列模式工具正向分析HUD系统的性能以及后续可能的扩展分析。 上两篇文章中(第一部分点此查看&#xff0c;第二部分点此查看)&#xff0c;我们主要介绍了如何以逆…