美文网首页Android Kotlin
密封类sealed class

密封类sealed class

作者: 长点点 | 来源:发表于2023-03-11 07:18 被阅读0次

    一、什么是kotlin密封类?

    密封类是一种特殊的类,它用来表示受限的类继承结构,即一个类只能有有限的几种子类,而不能有任何其他类型的子类。

    密封类使用sealed关键字声明,在Kotlin 1.0中,密封类的所有子类必须嵌套在密封类内部;在Kotlin 1.1中,这个限制放宽了,允许将子类定义在同一个文件中;在Kotlin 1.5中,这个限制进一步放宽了,允许将子类定义在任何地方,只要保证子类可见性不高于父类。

    密封类最大的优点是可以配合when表达式实现完备性检查(exhaustive check),即编译器可以检测出是否覆盖了所有可能的分支情况,如果没有则会报错或提示警告。这样可以避免遗漏某些情况导致逻辑错误或运行时异常。

    例如,我们可以定义一个表示运算符的密封类,以及它的四个子类:

    //定义一个密封类
    sealed class Operator
    
    //定义四个子类,分别表示加、减、乘、除
    class Add : Operator()
    class Subtract : Operator()
    class Multiply : Operator()
    class Divide : Operator()
    

    二、kotlin密封类的优缺点

    2.1 优点

    • 密封类可以让我们在编译时就确定所有可能的子类类型,从而提高代码的安全性和可读性。
    • 密封类可以让我们在使用when表达式时,不需要写else分支,因为所有的情况都已经覆盖了,这样可以减少代码的冗余和错误。
    • 密封类可以让我们在设计模式中,更方便地实现一些模式,例如状态模式、访问者模式等。

    2.2 缺点

    • 密封类的子类必须在同一个文件中声明,或者嵌套在密封类的声明中,这样可能会导致文件过长或者结构不清晰。
    • 密封类的子类不能被其他类继承,这样可能会限制了代码的复用性和扩展性。

    三、kotlin密封类的使用方式

    密封类适合用于表示有限的几种类型的情况,例如:

    • 表示状态或者结果,例如成功、失败、加载中等。
    • 表示运算符或者表达式,例如加、减、乘、除等。
    • 表示事件或者动作,例如点击、滑动、拖拽等。

    下面我们用一个简单的示例来演示如何使用密封类。我们定义一个表示计算器的密封类,它有两个子类,分别表示数字和运算符:

    //定义一个表示计算器的密封类
    sealed class Calculator
    
    //定义两个子类,分别表示数字和运算符
    data class Number(val value: Int) : Calculator()
    data class Operator(val op: String) : Calculator()
    

    然后我们定义一个函数,用来根据输入的密封类对象,计算结果:

    
    //定义一个函数,用来根据输入的密封类对象,计算结果
    fun calculate(a: Calculator, b: Calculator): Int {
        //使用when表达式,根据不同的子类类型,进行不同的操作
        return when (a) {
            //如果a是数字,再判断b的类型
            is Number -> when (b) {
                //如果b也是数字,就根据a的值和b的值进行运算
                is Number -> when (a.value) {
                    //如果a的值是0,就返回0
                    0 -> 0
                    //如果a的值是1,就返回b的值
                    1 -> b.value
                    //否则,就根据b的运算符进行运算
                    else -> when (b.op) {
                        //如果b的运算符是加号,就返回a的值加b的值
                        "+" -> a.value + b.value
                        //如果b的运算符是减号,就返回a的值减b的值
                        "-" -> a.value - b.value
                        //如果b的运算符是乘号,就返回a的值乘b的值
                        "*" -> a.value * b.value
                        //如果b的运算符是除号,就返回a的值除b的值
                        "/" -> a.value / b.value
                        //其他情况,就抛出异常
                        else -> throw IllegalArgumentException("Invalid operator: ${b.op}")
                    }
                }
                //如果b是运算符,就抛出异常
                is Operator -> throw IllegalArgumentException("Cannot calculate two operators")
            }
            //如果a是运算符,就抛出异常
            is Operator -> throw IllegalArgumentException("Cannot calculate an operator and a number")
        }
    }
    

    四、密封类与设计模式

    1.状态模式

    状态模式是一种行为型设计模式,它允许一个对象在其内部状态改变时改变它的行为。状态模式将状态封装成独立的类,并将请求委托给当前的状态对象,从而实现状态的切换和行为的变化。

    1.1kotlin实现

    密封类可以表示一个有限的状态集合,而且可以在when表达式中进行完备的检查,不需要使用else分支。例如,假设有一个电灯类,它有两种状态:开和关,每种状态下可以执行不同的操作。可以用以下的代码来实现:

    // 定义一个密封类表示状态
    sealed class State {
        // 定义一个抽象方法表示操作
        abstract fun operate()
    }
    
    // 定义一个开状态的子类,继承自State
    class On : State() {
        // 重写操作方法,打印信息并切换到关状态
        override fun operate() {
            println("The light is on, turn it off.")
            Light.state = Off()
        }
    }
    
    // 定义一个关状态的子类,继承自State
    class Off : State() {
        // 重写操作方法,打印信息并切换到开状态
        override fun operate() {
            println("The light is off, turn it on.")
            Light.state = On()
        }
    }
    
    // 定义一个电灯类,它有一个状态属性,初始为关状态
    object Light {
        var state: State = Off()
    
        // 定义一个方法,根据当前状态执行操作
        fun switch() {
            state.operate()
        }
    }
    
    // 测试代码
    fun main() {
        // 创建一个电灯对象
        val light = Light
        // 调用switch方法,根据当前状态执行操作
        light.switch() // The light is off, turn it on.
        light.switch() // The light is on, turn it off.
        light.switch() // The light is off, turn it on.
    }
    
    1.2java实现

    在java中,实现状态模式的一种方式是使用枚举类,因为枚举类也可以表示一个有限的状态集合,而且可以实现接口或抽象类,从而定义不同的行为。例如,用java实现上面的例子,可以用以下的代码:

    // 定义一个接口表示状态
    interface State {
        // 定义一个抽象方法表示操作
        void operate();
    }
    
    // 定义一个枚举类表示状态,实现State接口
    enum LightState implements State {
        // 定义两个枚举常量,分别表示开和关状态
        // 在每个枚举常量的构造器中,传入一个State对象,表示该状态下的行为
        ON(new State() {
            // 重写操作方法,打印信息并切换到关状态
            @Override
            public void operate() {
                System.out.println("The light is on, turn it off.");
                Light.state = OFF;
            }
        }),
        OFF(new State() {
            // 重写操作方法,打印信息并切换到开状态
            @Override
            public void operate() {
                System.out.println("The light is off, turn it on.");
                Light.state = ON;
            }
        });
    
        // 定义一个私有的State属性,表示该枚举常量对应的状态对象
        private final State state;
    
        // 定义一个私有的构造器,接收一个State对象作为参数,赋值给state属性
        private LightState(State state) {
            this.state = state;
        }
    
        // 定义一个公共的方法,调用state属性的operate方法
        public void operate() {
            state.operate();
        }
    }
    
    // 定义一个电灯类,它有一个状态属性,初始为关状态
    class Light {
        // 定义一个静态的LightState属性,表示电灯的状态,初始为OFF
        public static LightState state = LightState.OFF;
    
        // 定义一个方法,根据当前状态执行操作
        public void switch() {
            state.operate();
        }
    }
    
    // 测试代码
    public class Main {
        public static void main(String[] args) {
            // 创建一个电灯对象
            Light light = new Light();
            // 调用switch方法,根据当前状态执行操作
            light.switch(); // The light is off, turn it on.
            light.switch(); // The light is on, turn it off.
            light.switch(); // The light is off, turn it on.
        }
    }
    

    可以看出,使用kotlin的密封类实现状态模式的优点是:

    • 代码更简洁,不需要定义接口或抽象类,也不需要使用匿名内部类
    • 代码更安全,不需要担心在其他地方出现未知的状态,也不需要使用else分支处理默认情况
    • 代码更易读,可以清楚地看出状态之间的层次关系和转换逻辑

    使用java的枚举类实现状态模式的优点是:

    • 代码更统一,不需要在不同的类中定义状态,也不需要使用对象来表示状态
    • 代码更高效,不需要创建多个状态对象,也不需要使用多态来调用操作方法

    2. 访问者模式

    访问者模式是一种行为型设计模式,它允许在不修改已有类的结构的情况下,定义作用于这些类的新操作。访问者模式将元素的结构和元素的操作分离,使得操作可以根据不同的元素类型而变化。

    2.1kotlin实现

    在kotlin中,可以使用密封类来实现访问者模式,因为密封类可以表示一个有限的元素集合,而且可以在when表达式中进行完备的检查,不需要使用else分支。例如,假设有一个表达式类,它有两种子类:数字和加法,每种子类都可以被访问者访问,执行不同的操作。可以用以下的代码来实现:

    // 定义一个密封类表示表达式
    sealed class Expr {
        // 定义一个抽象方法表示接受访问者的访问
        abstract fun <R> accept(visitor: Visitor<R>): R
    }
        // 定义一个数字的子类,继承Expr类,有一个value属性表示数字的值
        data class Num(val value: Int) : Expr() {
            // 重写accept方法,调用访问者的visitNum方法,传入自己作为参数
            override fun <R> accept(visitor: Visitor<R>): R {
                return visitor.visitNum(this)
            }
        }
    
        // 定义一个加法的子类,继承Expr类,有两个Expr属性表示左右操作数
        data class Sum(val left: Expr, val right: Expr) : Expr() {
            // 重写accept方法,调用访问者的visitSum方法,传入自己作为参数
            override fun <R> accept(visitor: Visitor<R>): R {
                return visitor.visitSum(this)
            }
        }
    
        // 定义一个访问者接口,泛型参数R表示访问的结果类型
        interface Visitor<R> {
            // 定义一个访问数字的方法,接收一个Num对象作为参数,返回一个R类型的结果
            fun visitNum(num: Num): R
            // 定义一个访问加法的方法,接收一个Sum对象作为参数,返回一个R类型的结果
            fun visitSum(sum: Sum): R
        }
    
        // 定义一个求值的访问者类,实现Visitor接口,泛型参数为Int
        class EvalVisitor : Visitor<Int> {
            // 重写访问数字的方法,返回数字的值
            override fun visitNum(num: Num): Int {
                return num.value
            }
            // 重写访问加法的方法,返回左右操作数的求值结果的和
            override fun visitSum(sum: Sum): Int {
                return sum.left.accept(this) + sum.right.accept(this)
            }
        }
    
        // 定义一个打印的访问者类,实现Visitor接口,泛型参数为String
        class PrintVisitor : Visitor<String> {
            // 重写访问数字的方法,返回数字的字符串表示
            override fun visitNum(num: Num): String {
                return num.value.toString()
            }
            // 重写访问加法的方法,返回加法的字符串表示,用括号括起来
            override fun visitSum(sum: Sum): String {
                return "(${sum.left.accept(this)} + ${sum.right.accept(this)})"
            }
        }
    
        // 测试代码
        fun main() {
            // 创建一个表达式对象,表示1 + (2 + 3)
            val expr = Sum(Num(1), Sum(Num(2), Num(3)))
            // 创建一个求值的访问者对象
            val evalVisitor = EvalVisitor()
            // 创建一个打印的访问者对象
            val printVisitor = PrintVisitor()
            // 调用表达式的accept方法,传入求值的访问者,打印求值的结果
            println(expr.accept(evalVisitor)) // 6
            // 调用表达式的accept方法,传入打印的访问者,打印表达式的字符串表示
            println(expr.accept(printVisitor)) // (1 + (2 + 3))
        }
    

    可以看出,使用kotlin的密封类实现访问者模式的优点是:

    • 代码更简洁,不需要定义抽象方法或接口,也不需要使用类型转换或类型检查
    • 代码更安全,不需要担心在其他地方出现未知的表达式类型,也不需要使用else分支处理默认情况
    • 代码更易读,可以清楚地看出表达式之间的层次关系和访问者的操作逻辑
    2.2 java实现
    // 抽象元素
    interface Animal {
        void accept(Visitor visitor); // 接受一个抽象访问者
    }
    
    // 具体元素Lion
    class Lion implements Animal {
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this); // 调用具体访问者对自己进行操作
        }
    
        public String roar() {
            return "Roar!";
        }
    }
    
    // 具体元素Tiger
    class Tiger implements Animal {
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this); // 调用具体访问者对自己进行操作
        }
    
        public String growl() {
            return "Growl!";
        }
    }
    
    // 具体元素Elephant
    class Elephant implements Animal {
        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this); // 调用具体访问者对自己进行操作
        }
    
        public String trumpet() {
            return "Trumpet!";
        }
    }
    
    // 抽象访问者
    interface Visitor {
        void visit(Lion lion); // 访问具体元素Lion
    
        void visit(Tiger tiger); // 访问具体元素Tiger
    
        void visit(Elephant elephant); // 访问具体元素Elephant
    }
    
    // 具体访问者Feed
    class Feed implements Visitor {
      @Override 
      public void visit(Lion lion) { 
          System.out.println("Feed the lion with meat.");
      } 
    
      @Override 
      public void visit(Tiger tiger) { 
          System.out.println("Feed the tiger with chicken.");
      } 
    
      @Override 
      public void visit(Elephant elephant) { 
          System.out.println("Feed the elephant with banana.");
      } 
    }
    
    // 具体访问者Observe
    class Observe implements Visitor { 
      @Override 
      public void visit(Lion lion) { 
          System.out.println("Observe the lion: " + lion.roar());
      } 
    
      @Override 
      public void visit(Tiger tiger) { 
          System.out.println("Observe the tiger: " + tiger.growl());
      } 
    
      @Override 
      public void visit(Elephant elephant) { 
          System.out.println("Observe the elephant: " + elephant.trumpet());
      }  
    }
    
    // 具体访问者Train
    class Train implements Visitor {  
       @Override  
       public void visit(Lion lion) {  
           System.out.println("Train the lion to jump through a hoop.");  
       }  
    
       @Override  
       public void visit(Tiger tiger) {  
           System.out.println("Train the tiger to stand on a ball.");  
       }  
    
       @Override  
       public void visit(Elephant elephant) {  
           System.out.println("Train the elephant to sit on a stool.");  
       }   
    }  
    
    // 对象结构类Zoo
    
    class Zoo {
    
     private List<Animal> animals = new ArrayList<>();
    
     // 添加一个新动物   
    public void add(Animal animal) {   
         animals.add(animal);   
    }   
    
     // 移除一个已有动物   
    public void remove(Animal animal) {   
         animals.remove(animal);   
    }   
    
     // 接受一个抽象访问者,并将所有动物传递给它进行处理    
    public void accept(Visitor visitor) {    
         for (Animal animal : animals) {    
             animal.accept(visitor);    
         }    
    }   
    }
    

    测试代码:

    public class Test {
    
       public static void main(String[] args) {
    
           Zoo zoo = new Zoo();
    
           zoo.add(new Lion());
           zoo.add(new Tiger());
           zoo.add(new Elephant());
    
           Visitor feed = new Feed();
           Visitor observe = new Observe();
           Visitor train = new Train();
    
           zoo.accept(feed);
           zoo.accept(observe);
           zoo.accept(train);
       }
    }
    

    运行测试代码并显示输出结果:

    Feed the lion with meat.
    Feed the tiger with chicken.
    Feed the elephant with banana.
    Observe the lion: Roar!
    Observe the tiger: Growl!
    Observe the elephant: Trumpet!
    Train the lion to jump through a hoop.
    Train the tiger to stand on a ball.
    Train the elephant to sit on a stool.
    

    五、与final类对比

    特性 密封类 final类
    可继承性 部分可继承,只能被指定的类继承 不可继承
    受保护成员或虚成员 不允许,因为密封类的子类必须是密封类或final类 允许,因为final类没有子类
    抽象性 允许,因为密封类可以是抽象的或具体的 不允许,因为抽象类必须被继承
    相似之处 都是限制类的继承,都不能声明为抽象的,都可以继承别的类或接口 都是限制类的继承,都不能声明为抽象的,都可以继承别的类或接口
    不同之处 可以指定哪些类可以作为其子类,可以实现多态性,可以用于一些特定的设计模式 不能有任何子类,不能实现多态性,不能用于一些特定的设计模式
    优点 可以保证封装性和多态性,可以提高代码的可读性和可维护性,可以避免不必要的继承或实现 可以保证封装性和不变性,可以提高代码的执行效率,可以避免类的滥用
    缺点 可能增加代码的复杂度和冗余,可能限制类的扩展性和灵活性,可能与一些框架或库不兼容 可能增加代码的耦合度和僵化度,可能限制类的扩展性和灵活性,可能与一些框架或库不兼容
    应用场景 可以用于实现一些特定的设计模式,例如状态模式、策略模式、访问者模式等,这些模式需要明确地定义一组有限的子类或实现类 可以用于实现一些不需要继承的类,例如工具类、常量类、单例类等,这些类需要保证其不变性和唯一性

    相关文章

      网友评论

        本文标题:密封类sealed class

        本文链接:https://www.haomeiwen.com/subject/kuhmldtx.html