枚举与应用

news2024/12/26 11:10:00
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

枚举简介

枚举是一种特殊的类,和class、interface等是一个级别的(其实就是一个类),一般用于表示多种固定的状态。

《阿里巴巴开发手册》对枚举的介绍:

我们在设计山寨枚举中用WeekDay表示周一到周日,但正版枚举的格式要精简很多:

注意,WeekDayEnum命名规则并不是固定的,但推荐以Enum结尾,方便见名知意。当一个类声明为enum时,表示这是一个枚举类,比如enum WeekDayEnum。

对于枚举为何出现,你可以理解为JDK1.5以前很多人和我们之前一样都采用自定义山寨枚举的办法制作枚举类,所以JDK干脆提供了官方版,而且JDK枚举的底层原理正是“抄袭”了我们的山寨枚举。

枚举类底层原理

我们对上面的WeekDayEnum反编译:

是不是和我们山寨"枚举"很相似?

区别在于:

  • enum关键字修饰的WeekDayEnum,在编译后会自动继承Enum类,且声明为final
  • 比山寨版枚举多了valueOf()方法,传入名称返回对应的枚举实例
  • values()并不是直接返回WeekDay[],而是$VALUES.clone()

特别是最后一点,我们来对比下。山寨版枚举:

正版枚举反编译:

为什么正版的枚举要$VALUES.clone()呢?

因为如果像山寨版枚举这样返回整个实际数组,外部调用者就可以随意增删枚举对象!

另外,我们可以尝试分别打印正版枚举和我们的山寨枚举:

说明正版枚举重写了toString()方法。

光看WeekDayEnum的代码好像看不出来哪里重写了toString(),于是推测肯定是它的父类Enum重写了:

在Enum这里,有且仅有一个构造器:

而我们都知道子类在new的时候,会调用父类的构造器。

所以子类WeekDayEnum在哪里调了这个构造器、又往它里面传入了什么呢?

我们发现,虽然当初编写WeekDayEnum时,构造器只传入了两个参数,但编译器却给我们额外加了两个(见图一),用来为父类Enum中的name和ordinal赋值(见图二)。name来自MONDAY、TUESDAY等枚举名称,而ordinal则是编写序号。

特别注意,name并不是MONDAY(1, "星期一")里的desc,而是MONDAY字符串。这很好理解,因为不是所有枚举都有desc字段,它是我们自己定义的字段。如果你愿意,完全可以这样定义枚举:

枚举中的每个对象都有默认的name,就是你看到的通常意义上的枚举名称,而默认toString()会打印name。

name和ordinal

勇敢面对内心的疑问吧:为什么编译器要给构造器额外添加参数name、ordinal?

这个问题其实我们在设计山寨版枚举时讨论过了:

总的来说,就是为了区分枚举对象。因为在编写枚举时,极有可能会这样写:

这个是无参的,反编译后如果不额外添加name、ordinal,就会变成这样:

此时如果你把整个枚举存入数据库,就会都变成"{}",那么下次取出来就没什么特征性了。

另一个原因是,新增的参数是为了满足valueOf()方法,调用者可以传入字符串,匹配对应的枚举类型。比如

FruitEnum BANANA = FruitEnum.valueOf("BANANA")

这样就可以根据name得到BANANA枚举(对象)。

枚举 VS 常量类

枚举和常量类有什么区别呢?

我想起了另一个经常会被问到的问题:

Dubbo和SpringCloud什么区别?

Dubbo只是一个RPC框架,而SpringCloud几乎代表了一整个微服务解决方案,从网关、注册中心到配置中心一应俱全,完全不是一个概念。

同样的,枚举本质是一个对象,可以设置对象该有的一切,而常量类往往只有字段(public static final),枚举对象和常量类的字段是没有可比性的。比如枚举可以自定义方法,常量可以吗?

总之,应该把枚举看成特殊的对象,而不是仅仅将其等同于常量。

作为常量使用时,区别不大

如果实际编码时传递的是枚举的字段而不是枚举本身,比如WeekDayEnum.TUESDAY.getCode(),此时枚举和常量类的用法差别不大。

甚至单纯作为常量用的话,常量类似乎更容易理解且方便。

枚举优点一:限制重复定义常量

枚举作为常量使用时,编译器会自动限制取值不能相同,而常量类做不到,有可能会重复

其实这个优点没什么卵用,凑数。

枚举优点二:包含更多维度的信息

枚举毕竟是对象,可以包含更多的信息

枚举优点三:枚举+Switch比常量+if/else简洁

枚举可以直接用于Switch判断,代码通常会比常量+if/else简洁一些:

至此终于把上面三个“优点”讲完,这也是网上抄来抄去的一些内容。实话实说,我不觉得if/else就怎么了。我的想法是,上面的优点好像都是强行凑数,用来捧枚举,然后踩一脚常量类,个人认为没什么卵用。

枚举真要谈优点,一定要从它作为对象的角度来说,而不是强行把它限定为“特殊的常量类”。

利用枚举完善状态码

枚举做统一返回结果,可以有效规范错误码及错误信息。我们在山寨版枚举中演示过了。

如果不用枚举,code和desc做不到绑定,用户传入1的同时可以传入"参数错误",这样状态码和信息就错乱了。

所以最好不要这样设计:

状态码和状态描述之间没有必然联系,是散乱的。

枚举策略

再讲一个利用枚举消除if/else的案例,具体这是什么设计模式其实不重要,不管黑猫白猫,能抓老鼠就是好猫。

比如一个网上商城有会员制度:

  • 黄金会员:6折
  • 白银会员:7折
  • 青铜会员:8折

无论在商城里买什么,都会按照会员的折扣优惠。

用常量类实现:

public class MemberDemo {
    public static void main(String[] args) {
        User user = new User(1L, "bravo", Constants.GOLD_MEMBER);
        BigDecimal productPrice = new BigDecimal("1000");
        BigDecimal discountedPrice = calculateFinalPrice(productPrice, user.getMemberType());
        System.out.println(discountedPrice);
    }

    /**
     * 根据会员身份返回折扣后的商品价格
     *
     * @param originPrice
     * @param user
     * @return
     */
    public static BigDecimal calculateFinalPrice(BigDecimal originPrice, Integer type) {
        if (Constants.GOLD_MEMBER.equals(type)) {
            return originPrice.multiply(new BigDecimal("0.6"));
        } else if (Constants.SILVER_MEMBER.equals(type)) {
            return originPrice.multiply(new BigDecimal("0.7"));
        } else if (Constants.BRONZE_MEMBER.equals(type)) {
            return originPrice.multiply(new BigDecimal("0.8"));
        } else {
            return originPrice;
        }
    }
}

@Data
@AllArgsConstructor
class User {
    private Long id;
    private String name;
    /**
     * 会员身份
     * 1:黄金会员,6折优惠
     * 2:白银会员,7折优惠
     * 3:青铜会员,8折优惠
     */
    private Integer memberType;
}

class Constants {
    /**
     * 黄金会员
     */
    public static final Integer GOLD_MEMBER = 1;
    /**
     * 白银会员
     */
    public static final Integer SILVER_MEMBER = 2;
    /**
     * 青铜会员
     */
    public static final Integer BRONZE_MEMBER = 3;
}

上面的代码有两个缺点:

  • if/else过多,如果会员制度再复杂些,语句会更长,阅读性不佳
  • 不利于扩展。如果后期和百度一样不要脸地在vip基础上新增svip,需要修改逻辑判断的代码,这很容易出错

用枚举实现(在枚举类中定义抽象方法,强制子类实现):

/**
 * @author mx
 * @date 2023-11-25 14:57
 */
public class MemberDemo {
    public static void main(String[] args) {
        User user = new User(1L, "bravo", MemberEnum.GOLD_MEMBER.getType());
        BigDecimal productPrice = new BigDecimal("1000");
        BigDecimal discountedPrice = calculateFinalPrice(productPrice, user.getMemberType());
        System.out.println(discountedPrice);
    }

    /**
     * 根据会员身份返回折扣后的商品价格
     *
     * @param originPrice
     * @param user
     * @return
     */
    public static BigDecimal calculateFinalPrice(BigDecimal originPrice, Integer type) {
        return MemberEnum.getEnumByType(type).calculateFinalPrice(originPrice);
    }
}

@Data
@AllArgsConstructor
class User {
    private Long id;
    private String name;
    /**
     * 会员身份
     * 1:黄金会员,6折优惠
     * 2:白银会员,7折优惠
     * 3:青铜会员,8折优惠
     */
    private Integer memberType;
}

enum MemberEnum {
    // ---------- 把这几个枚举当做本应该在外面实现的MemberEnum子类,不要看成MemberEnum内部的 ----------
    GOLD_MEMBER(1, "黄金会员") {
        @Override
        public BigDecimal calculateFinalPrice(BigDecimal originPrice) {
            return originPrice.multiply(new BigDecimal(("0.6")));
        }
    },
    SILVER_MEMBER(2, "白银会员") {
        @Override
        public BigDecimal calculateFinalPrice(BigDecimal originPrice) {
            return originPrice.multiply(new BigDecimal(("0.7")));
        }
    },
    BRONZE_MEMBER(3, "青铜会员") {
        @Override
        public BigDecimal calculateFinalPrice(BigDecimal originPrice) {
            return originPrice.multiply(new BigDecimal(("0.8")));
        }
    },
    ;

    // ---------- 下面才是MemberEnum类的定义 ---------
    private final Integer type;
    private final String desc;

    MemberEnum(Integer type, String desc) {
        this.type = type;
        this.desc = desc;
    }

    /**
     * 定义抽象方法,留个子类实现
     *
     * @param originPrice
     * @return
     */
    protected abstract BigDecimal calculateFinalPrice(BigDecimal originPrice);

    public Integer getType() {
        return type;
    }

    public String getDesc() {
        return desc;
    }

    public static MemberEnum getEnumByType(Integer type) {
        MemberEnum[] values = MemberEnum.values();
        for (MemberEnum memberEnum : values) {
            if (memberEnum.getType().equals(type)) {
                return memberEnum;
            }
        }
        throw new IllegalArgumentException("Invalid Enum type:" + type);
    }
}

枚举类本身比较抽象,上面的代码可能一部分人会一时转不过弯。那你就想象着把三个枚举抽出来:

当然,你也可以新增一个接口,在接口内部定义calculateFinalPrice(),让MemberEnum实现该接口

我敢肯定,必然有部分同学觉得我在故弄玄虚,搞了一大堆,但看起来反而更麻烦。

大家不要光看当前的代码量,而要考虑扩展。如果此时需要新增会员类型,那么使用常量的版本需要改动两处:

  • 常量类
  • if/else所在的类

而枚举版本只要在枚举类中新增一个类型:

修改if/else的犯错概率一般远大于新增枚举类。这其实也体现了设计模式的思想:用增量代替修改。

还是觉得枚举麻烦?

此时应该给代码加上一根时间轴,动态地看待问题。比如,假设随着时间推移,不可避免地需要在很多地方对用户的身份进行判断,于是你的同事把你写的if/else拷贝到项目的各个角落。

而产品的需求改到第二版时才发现漏算了星耀会员,于是你需要找到所有相关的if/else进行修改...你必须细致,只要有一处忘了改就会出错,增加了测试和维护的成本。

但如果采用策略枚举的方式,把易变动的逻辑抽取出来共同维护就会方便很多。

设计模式中有个说法:越抽象越稳定,越具体越不稳定,所以提倡面向抽象编程。设计模式的目的不是消除变化,而是隔离变化。在软件工程中,变化的代码就像房间里一只活蹦乱跳的兔子,你并不能让它绝对安静(不要奢望需求永不变更),但可以准备一个笼子把它隔离起来,从而达到整体的稳定。

上面的代码中,if/else是兔子,而MemberEnum则扮演着笼子的角色。如果不用MemberEnum把兔子关在一起,那么兔子就会在整个项目蹦跶,很难管理。

上面案例充分说明作为对象的枚举作为字段的常量可玩性高很多。

枚举代码优化

优化1:尽量使用foreach

举个例子,对于普通的for循环:

每次循环其实都会判断i<list.size(),每次都会调用size()获取大小,而foreach则会进行优化:

其他写法:

如果你学过Java8的Stream,还可以这样写:

抛不抛异常看业务,个人觉得返回null意义也不大,反正要么空指针要么插入不可预计的值。

优化2:用Map替代For,缓存枚举值,利用key提高检索速度(实用小算法)

这种写法是我在研究MyBatis枚举转换器时看源码发现的:

优化3:静态导入简化代码(看个人喜好,不是很好用)

一般都是这样写:

但有时代码会看起来很长,此时可以使用静态导入:

优化4:结合Lombok简化代码

@Getter
@AllArgsConstructor
public enum PlatformEnum {
    TAOBAO(1, "淘宝", "taobao", "http://xxx"),
    PDD(2, "拼多多", "pdd", "http://xxx");

    private final Integer code;
    private final String sourceKey;
    private final String iconUrl;

    public static PlatformEnum getByCode(Integer code) {
        for (PlatformEnum platformEnum : PlatformEnum.values()) {
            if (platformEnum.getCode().equals(code)) {
                return platformEnum;
            }
        }
        return null;
    }
}

枚举与常量类的取舍

最后说一下个人平时对枚举和常量类的取舍:

  • 错误码的定义用枚举
  • 字段状态用枚举或常量类都可以,个人更倾向于使用枚举,可玩性会比常量类高
  • 一些全局变量的定义可以使用常量类,比如USER_INFO、SESSION_KEY

一点小建议

大部分人设计枚举时都是使用以下格式:

MONDAY(1, "星期一")

这种格式在当前情境下是合理的,但如果是其他场景:

WAIT_FOR_DEAL(1, "等待处理")

SUITABLE(2, "合适")

UNSUITABLE(3, "不合适")

INVITE(4, "邀请面试")

...

随着状态越来越多,前端同学会变得很痛苦。所以如果你的项目允许,可以使用MONDAY("MONDAY", "星期一")这种形式,也就是存字符串。

这个时候其实没必要考虑索引键长问题,你就当它是一个普通字段。退一步想,如果这是name字段,难道你会因为索引的原因而使用int类型吗...考虑前后端对接及维护,代码的可读性有时比一丁点所谓的性能还重要。

扩展内容

在网上看到的写法,但个人觉得不是很好。他的出发点是每个枚举类都要写getEnumByType()方法,太麻烦了,希望把各个类中的getEnumByXxx()方法抽取到工具类中:

public class EnumUtil {
    private static final Map<Object, Object> key2EnumMap = new ConcurrentHashMap<>();
    private static final Set<Class<?>> enumSet = ConcurrentHashMap.newKeySet();

    /**
     * 获取枚举(带缓存)
     *
     * @param enumType          枚举Class类型
     * @param enumToValueMapper 抽取字段(按哪个字段搜索)
     * @param key               待匹配的字段值
     * @param <T>
     * @return
     */
    public static <T extends Enum<T>, R> Optional<T> getEnumWithCache(Class<T> enumType, Function<T, R> enumToValueMapper, Object key) {
        if (!enumSet.contains(enumType)) {
            // 不同的枚举类型互相不影响
            synchronized (enumType) {
                if (!enumSet.contains(enumType)) {
                    // 添加枚举
                    enumSet.add(enumType);
                    // 缓存枚举键值对
                    for (T enumConstant : enumType.getEnumConstants()) {
                        // enumToValueMapper.apply()表示从枚举中获得value。但不同枚举可能value相同,因此getKe()进一步加工,为value拼接路径做前缀
                        String mapKey = getKey(enumType, enumToValueMapper.apply(enumConstant));
                        key2EnumMap.put(mapKey, enumConstant);
                    }
                }
            }
        }

        return Optional.ofNullable((T) key2EnumMap.get(getKey(enumType, key)));
    }


    /**
     * 获取key
     * 注:带上枚举类名作为前缀,避免不同枚举的Key重复
     *
     * @param enumType
     * @param key
     * @param <T>
     * @return
     */
    public static <T extends Enum<T>, R> String getKey(Class<T> enumType, R key) {
        return enumType.getName().concat(key.toString());
    }

    /**
     * 获取枚举(不缓存)
     *
     * @param enumType
     * @param enumToValueMapper
     * @param key
     * @param <T>
     * @param <R>
     * @return
     */
    public static <T extends Enum<T>, R> Optional<T> getEnum(Class<T> enumType, Function<T, R> enumToValueMapper, Object key) {
        for (T enumThis : enumType.getEnumConstants()) {
            if (enumToValueMapper.apply(enumThis).equals(key)) {
                return Optional.of(enumThis);
            }
        }
        return Optional.empty();
    }

    // ----------- 测试工具类 ------------
    public static void main(String[] args) {
        // 按code搜索,寻找code=2的FruitEnum对象
        Optional<FruitEnum> fruitEnum = EnumUtil.getEnumWithCache(FruitEnum.class, FruitEnum::getCode, 2);
        fruitEnum.ifPresent(fruit -> System.out.println(fruit.getName()));
    }

    @Getter
    enum FruitEnum {
        APPLE(1, "苹果"),
        BANANA(2, "香蕉"),
        ORANGE(3, "橘子"),
        ;

        FruitEnum(Integer code, String name) {
            this.code = code;
            this.name = name;
        }

        private final Integer code;
        private final String name;
    }
}

为什么我认为上面的Util工具不好呢?

  • 首先,getEnumWithCache()最后返回值需要强转,IDEA提示看着很难受。但似乎无法解决,因为key2EnumMap只能用Object,所以注定了需要强转。
  • 其次,也是最重要的,没有对Function enumToValueMapper做限制。也就是说用户可以随意传入映射参数,有些人把枚举映射为name,有些人把枚举映射为oridnal,还有其他属性。也就说,很乱。到时取出时,你都不知道自己当初存的key是什么。

当然,如果你实在需要一个EnumUtil简化操作,那么下面这个简化版的可能更适合你(其实hutool这类第三方工具都有提供类似的功能):

@Slf4j
public class EnumUtil {
    
    /**
     * 获取枚举类型的枚举值,通过属性值
     *
     * @param clazz         枚举类
     * @param fieldFunction 属性值获取函数
     * @param fieldValue    属性值
     * @param <E>           枚举类
     * @param <V>           属性值
     * @return 匹配的枚举值,可能为null
     */
    public static <E extends Enum<E>, V> E getEnum(Class<E> clazz, Function<E, V> fieldFunction, V fieldValue) {
        Assert.notNull(clazz);
        E[] es = clazz.getEnumConstants();
        for (E e : es) {
            if (Objects.equals(fieldFunction.apply(e), fieldValue)) {
                return e;
            }
        }
        return null;
    }
    
    /**
     * 获取枚举类型的枚举值,通过属性值
     *
     * @param clazz         枚举类
     * @param fieldFunction 属性值获取函数
     * @param fieldValue    属性值
     * @param <E>           枚举类
     * @param <V>           属性值
     * @return 匹配的枚举值,Optional类型
     */
    public static <E extends Enum<E>, V> Optional<E> getEnumOptional(Class<E> clazz, Function<E, V> fieldFunction, V fieldValue) {
        Assert.notNull(clazz);
        E[] es = clazz.getEnumConstants();
        for (E e : es) {
            if (Objects.equals(fieldFunction.apply(e), fieldValue)) {
                return Optional.of(e);
            }
        }
        return Optional.empty();
    }
}

补充:解答读者疑惑

在本文评论区看到一个提问,觉得也是很多人的疑问。这里特别补充一下。

为什么枚举类内部可以定义抽象方法?

关于这个问题,可以先去看看JDK的Enum是什么样的:

Enum本身就是一个抽象类,却没有定义抽象方法。按之前小册讲过的:一个类,没有抽象方法,却被定义为抽象类,主要是为了不被new。同理 UserStatusEnum extends Enum后,自己也是一个抽象类,也没有任何抽象方法。

OK,回到问题本身。

我们通常说的 枚举,一般是指枚举对象,而不是枚举类。

@Getter
@AllArgsConstructor
public enum UserStatusEnum { // 这个才是枚举类

    // 这两个看起来是“字段”的玩意儿,其实是对象(枚举对象)
    DELETE(-1, "删除"),
    DEFAULT(0, "默认");

    private final Integer status;
    private final String desc;

}

所以,我在一个抽象类UserStatusEnum中定义一个抽象方法有什么问题呢?再者,既然我在UserStatusEnum中既然定义了抽象方法,那么DELETE、DEFAULT两个枚举对象就必须实现这个方法。

@Getter
@AllArgsConstructor
public enum UserStatusEnum {

    DELETE(-1 "删除"){
        // 实现UserStatusEnum定义的抽象方法
        @Override
        public void test() {
            
        }
    },
    DEFAULT(0, "默认") {
        @Override
        public void test() {
            
        }
    };

    private final Integer status;
    private final String desc;

    // 定义一个抽象方法
    public abstract void test();

}

枚举最令人困惑的地方在于,枚举类的定义和枚举对象是混在一个文件中的,再加上一些后期的自动编译处理,让初学者看起来觉得很抽象。但你如果把DELETE、DEFAULT单独拎出去,想象成一个独立的对象或实现类,会更容易接受。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

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

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

相关文章

软件测试:超详细的Jmeter基础教程

JMeter 介绍&#xff1a; 一个非常优秀的开源的性能测试工具。 优点&#xff1a;你用着用着就会发现它的重多优点&#xff0c;当然不足点也会呈现出来。 从性能工具的原理划分 Jmeter工具和其他性能工具在原理上完全一致&#xff0c;工具包含4个部分&#xff1a; &#xff…

是否有无限提取的代理IP?作为技术你需要知道这些

最近有互联网行业的技术小伙伴问到&#xff0c;有没有可以无限提取的代理IP&#xff1f;就是比如我一秒钟提取几万、几十万次&#xff0c;或者很多台机器同时调用API提取链接&#xff0c;这样可以吗&#xff1f;看到这个问题&#xff0c;不禁沉思起来&#xff0c;其实理论上是存…

数据结构与算法编程题23

设计二叉树的双序遍历算法&#xff08;双序遍历是指对于二叉树的每一个结点来说&#xff0c;先访问这个结点&#xff0c;再按双序遍历它的左子树&#xff0c;然后再一次访问这个结点&#xff0c;接下来按双序遍历它的右子树&#xff09; #define _CRT_SECURE_NO_WARNINGS#inclu…

【点云surface】Poisson表面重建

1 介绍 Poisson表面重建算法是一种用于从点云数据生成平滑曲面模型的算法。它基于Michael Kazhdan等人在2006年发表的论文《Poisson surface reconstruction》。该算法通过将点云数据转换为体素表示&#xff0c;并利用Poisson方程来重建曲面。 该算法的基本原理是将点云数据转…

python基础教程:动态参数

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 Python的动态参数有两种&#xff0c;分别是*args和**kwargs&#xff0c; 这里面的关键是一个和两个星号的区别&#xff0c;而不是args和kwargs在名字上的区别&#…

微信小程序-跳转到另一个小程序

微信小程序-跳转到另一个小程序 微信小程序跳转到另一个小程序有一个条件:这两个小程序被同一个微信公众号关联&#xff0c;否则不能跳转&#xff0c;会报错。 官方文档 wx.navigateToMiniProgram({appId: wxa38r249405b957c6,path: pages/splash/index,//extraData: 需要传递…

Android : PopupWindow 悬浮框_简单应用

示例图&#xff1a; MainActivity.java package com.example.popupwindow;import androidx.appcompat.app.AppCompatActivity;import android.content.Context; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.view.LayoutInflat…

浅谈STL中的分配器

分配器是STL中的六大部件之一&#xff0c;是各大容器能正常运作的关键&#xff0c;但是对于用户而言确是透明的&#xff0c;它似乎更像是一个幕后英雄&#xff0c;永远也不会走到舞台上来&#xff0c;观众几乎看不到它的身影&#xff0c;但是它又如此的重要。作为用户&#xff…

Vue组件的几种通信方式

这里写目录标题 Vue组件的几种通信&#xff08;数据传递&#xff09;方式非父子组件间通信&#xff08;Bus事件总线&#xff09;介绍实例 非父子通信-provide&inject1.作用2.场景3.语法4.注意 父子组件间的通信固定props属性名&#xff08;v-model&#xff09;介绍实例 不固…

【部署运维】docker:入门到进阶

0 前言 部署运维博客系列一共有三篇&#xff1a; 拥抱开源&#xff0c;将工作中的经验分享出来&#xff0c;尽量避免新手踩坑。 【部署运维】docker&#xff1a;入门到进阶 【部署运维】kubernetes&#xff1a;容器集群管理掌握这些就够了 【部署运维】pythonredisceleryd…

C++多态-虚函数

多态分为编译时多态和运行时多态。编译时多态就是在编译阶段就能绑定要执行的那个函 数。运行时多态要等到运行到调用的那条语句时&#xff0c;根据指针/引用所绑定的对象&#xff0c;来决定执行哪 个函数&#xff0c;我们要讲的虚函数就是运行时多态&#xff0c;是 C中非常重…

嵌入式的学习需要合理规划时间

低级的欲望放纵即可获得&#xff0c;高级的欲望只有克制才能达成。——卡耐基1、粉丝的误会 很多粉丝&#xff0c;问我&#xff0c; "胡老师我想报您的培训班。" ... 得知我知识业余时间写文章&#xff0c;紧接着又会问&#xff0c; "jg单位这么清闲啊&#…

带你理解H桥电路

H桥电路 文章目录 H桥电路前言一、H桥基本结构二、H桥原理二、控制模式三、MOS管H桥 前言 在做单片机控制小车的时候一定会用一个电机驱动板&#xff0c;这个电机驱动板是怎么做的呢&#xff0c;答案就是H桥&#xff0c;没学过电路的同学可能会问什么是H桥&#xff0c;这篇文章…

【c++哈夫曼树代码实现】

哈夫曼树是不定长编码方式&#xff0c;由于是将权值大的元素放在离根结点近的地方 &#xff0c;权值小的放在离根远的地方&#xff0c;哈夫曼树效率很高&#xff0c;并且一个编码不会以另一个编码作为前缀&#xff0c;避免了编码的歧义性&#xff0c;本文将带大家探索如何创建和…

SAP smartform 实现打印条形码

先在SE73里定义一个新的BARCODE&#xff0c;注意一定要用新的才可以&#xff0c;旧的是打印不出来的。 然后定义一个SMARTFORM的样式&#xff0c;把你定义的BARCODE放到字符样式里面去。 再做SMARTFORM就可以了&#xff0c;将需要作为条码的变量的格式选为该BARCODE格式&…

ASP.NET Core 使用 SignalR 实现实时通讯

&#x1f433;简介 SignalR是一个用于ASP.NET的库&#xff0c;它允许服务器代码向连接的客户端实时发送推送通知。它使用WebSockets作为底层传输机制&#xff0c;但如果浏览器不支持WebSockets&#xff0c;它会自动回退到其他兼容的技术&#xff0c;如服务器发送事件&#xff…

GEE:众数滤波

作者:CSDN @ _养乐多_ 在本文中,我们将介绍如何使用Google Earth Engine(GEE)平台对遥感影像进行众数滤波处理。并以众数滤波平滑NDVI图像为示例,演示众数滤波整个过程。 结果如下图所示, 文章目录 一、众数滤波二、完整代码三、代码链接一、众数滤波 众数滤波是一种图…

保护您的IP地址:预防IP地址盗用的关键措施

随着互联网的发展&#xff0c;IP地址作为标识互联网设备的重要元素&#xff0c;成为网络通信的基石。然而&#xff0c;IP地址盗用威胁正不断增加&#xff0c;可能导致敏感信息泄露、未经授权的访问和网络攻击。本文将介绍一些有效的方法&#xff0c;以帮助组织和个人预防IP地址…

【外贸干货】领英客户开发与营销的六个策略方向

领英(LinkedIn)已经成为外贸营销人员&#xff0c;尤其是B2B外贸营销人员&#xff0c;一个重要且有效的社交媒体平台。 相比于其他社交媒体平台&#xff0c;领英(LinkedIn)在增加流量、产生高质量的潜在客户和建立思想领导力方面有着独有的优势。 因为领英(LinkedIn)不仅仅是获…