Swift枚举

作者: 正_文 | 来源:发表于2022-09-03 12:17 被阅读0次

    枚举为一组相关的值定义了一个共同的类型,使你可以在你的代码中以类型安全的方式来使用这些值。
    我们熟悉 的C 语言,枚举会为一组整型值分配相关联的名称。Swift 中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为原始值),则该值的类型可以是字符串字符,或是一个整型值浮点数
    此外,枚举成员可以指定任意类型的关联值存储到枚举成员中,就像其他语言中的联合体(unions)和变体(variants)。你可以在一个枚举中定义一组相关的枚举成员,每一个枚举成员都可以有适当类型的关联值
    Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息;实例方法(instance methods),用于提供和枚举值相关联的功能;枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。

    一、枚举语法

    Swift 中,枚举支持IntDoubleString等基础类型,也有默认枚举值String类型默认枚举值为casekey名称,IntDouble数值型默认枚举值为0开始,+1递增。
    代码:

    // 写法一
    // 不需要逗号隔开
    enum Weak1 {
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case SAT
        case SUN
    }
    
    // 写法二
    // 也可以直接一个case,然后使用逗号隔开
    enum Weak2 {
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    
    // 定义一个枚举变量
    var w: Weak1 = .MON
    
    
    /*
    String类型的enum
    - =左边的值是枚举值,例如 MON
    - =右边的值在swift中称为 RawValue(原始值),例如 "MON"
    - 两者的关系为:case 枚举值 = rawValue原始值
    */
    enum Week: String{
        case MON = "MON"
        case TUE = "TUE"
        case WED = "WED"
        case THU = "THU"
        case FRI = "FRI"
        case SAT = "SAT"
        case SUN = "SUN"
    }
    

    如果不想写枚举值后的字符串,也可以使用隐式RawValue分配。

    enum Week: String{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    var w = Week.MON.rawValue
    print(w)
    
    //打印结果:MON
    
    1.2 枚举的访问

    那么Swift是如何获取rawValue的值,我们可以通过SIL文件分析。
    SIL命令:

    swiftc -emit-sil main.swift |xcrun swift-demangle >> ./main.sil && open main.sil
    

    SIL文件

    enum Week : String {
      case MON, TUE, WED, THU, FRI, SAT, SUN
      init?(rawValue: String)      //默认添加了一个可选类型的init方法
      typealias RawValue = String  //给枚举值的类型,通过typealias取了一个别名RawValue
      var rawValue: String { get } //增加一个计算属性rawValue,用于获取枚举值的原始值
    }
    

    main函数流程:

    image.png
    rawValuegetter方法: image.png
    bb8代码段: image.png
    总结:rawValue的底层就是调用的getter方法,getter方法中构造了字符串,但是这个字符串的值(例如“MON”)从哪里取出的呢?他们在编译期已经确定了,我们查看Mach-O,在TEXT的__cstring段就能看到。
    1.3 case枚举值 & rawValue原始值

    代码:

    //输出 case枚举值
    print(Week.MON)
    //输出 rawValue
    print(Week.MON.rawValue)
    //打印结果:MON  MON
    

    从结果来看是没有什么区别的,输出值都一样,但他们本质是不一样的。
    第一个,输出的case枚举值;第二个,是通过rawValue访问的rawValue的get方法
    下面这种写法,编译器就会报错:

    image.png
    1.4 枚举的init

    枚举的init会在什么时候调用,我们通过断点看一下:

    image.png
    ①,不设置Condition,进不到方法里面;②原生代码第一个断点,进入的是getter方法,进不去init
    即,enuminit方法的调用是通过枚举.init(rawValue:)或者枚举(rawValue:)触发的。

    继续:

    print(Week.init(rawValue: "MON"))
    print(Week.init(rawValue: "Hello"))
    
    //打印结果:
    //Optional(_6_EnumTest.Week.MON)
    //nil
    

    第一个输出的可选值,第二个输出的是nil。表示,没有找到对应的case枚举值
    分析SIL文件中的Week.init方法,主要有以下几步:
    1、在init方法中是将所有enum的字符串从Mach-O文件中取出,依次放入数组中;
    2、放完后,然后调用_findStringSwitchCase方法进行匹配。
    如下:

    2251862-4a1f18106a9065f7_.jpg
    index_addr:表示获取当前数组中的第n个元素值的地址,然后再把构建好的字符串放到当前地址中
    struct_extract:表示取出当前的Int值,Int类型在系统中也是结构体
    cond_br:表示比较的表达式,即分支条件跳转
        - 如果匹配成功,则构建一个 .some的Optional 返回
        - 如果匹配不成功,则继续匹配,知道最后还是没有匹配上,则构建一个.none的Optional返回
    

    _findStringSwitchCaseswift-source中,接收两个参数,分别是 数组 + 需要匹配的String。①遍历数组,如果匹配则返回对应的index;②如果不匹配,则返回-1
    所以,这也是为什么一个打印可选值,一个打印nil的原因。

    1.5枚举的遍历

    CaseIterable协议通常用于没有关联值的枚举,用来访问所有的枚举值,只需要对应的枚举遵守该协议即可,然后通过allCases获取所有枚举值,如下:

    // Double类型
    enum Week1: Double, CaseIterable {
        case Mon,Tue, Wed, Thu, Fri, Sat, Sun
    }
    Week1.allCases.forEach { print($0.rawValue)}
    
    // String类型
    enum Week2: String {
        case Mon,Tue, Wed, Thu, Fri, Sat, Sun
    }
    extension Week2: CaseIterable {}
    Week2.allCases.forEach { print($0.rawValue)}
    

    二、关联值枚举、模式匹配、属性方法

    如果希望用枚举表示复杂的含义,关联更多的信息,就需要使用关联值了。

    他与普通类型的枚举不同没有rawValue,没有rawValue的getter方法;没有初始化init方法

    //注:当使用了关联值后,就没有RawValue了
    //因为:case可以用一组值来表示,而rawValue是单个的值
    enum Shape{
        //case枚举值后括号内的就是关联值,如 radius
        case circle(radius: Double)
        case rectangle(width: Int, height: Int)
    }
    
    //创建
    var circle = Shape.circle(radius: 10.0)
    
    //重新分配
    circle = Shape.rectangle(width: 10, height: 10)
    
    模式匹配

    enum中的模式匹配其实就是匹配case枚举值,根据枚举类型,分为2种:
    1、简单类型的枚举的模式匹配;
    2、自定义类型的枚举(关联值)的模式匹配。

    简单enum的模式匹配

    注:swift中的enum模式匹配需要将所有情况都列举,或者使用default表示默认情况,否则会报错

    enum Week: String{
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case SAT
        case SUN
    }
    
    var current: Week?
    switch current {
        case .MON: print(Week.MON.rawValue)
        case .TUE: print(Week.MON.rawValue)
        default:print("unknow day")
    }
    
    //打印结果:unknow day
    
    关联值类型的模式匹配

    关联值类型模式匹配有两种方式:1、switch - case, 匹配所有case;2、if - case, 匹配单个case
    switch - case
    定义关联值枚举:

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Int, height: Int)
    }
    

    可以let var修饰关联值的入参:

    let shape = Shape.circle(radius: 10.0)
    switch shape{
        //相当于将10.0赋值给了声明的radius常量
        case let .circle(radius):
            print("circle radius: \(radius)")
        case .rectangle(let width, var height):
            height += 1
            print("rectangle width: \(width) height: \(height)")
    }
    
    • 通过if-case匹配单个case,如下:
    let circle = Shape.circle(radius: 10)
    
    //匹配单个case
    if case let Shape.circle(radius) = circle {
        print("circle radius: \(radius)")
    }
    
    • 如果我们只关心不同case的相同关联值(即关心不同case的某一个值),需要使用同一个参数
      例如,案例中的x,如果分别使用x、y, 编译器会报错:
    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Double, height: Double)
        case square(width: Double, height: Double)
    }
    let shape = Shape.circle(radius: 10)
    switch shape{
        case let .circle(x), let .square(20, x):
            print(x)
        default:
            break
    }
    

    也可以使用通配符_(表示匹配一切)的方式:

    let shape = Shape.rectangle(width: 10, height:20)
    switch shape{
    case let .rectangle(x, _), let .square(_, x):
        print("x = \(x)")
    default:
        break
    }
    

    注:枚举使用过程中不关心某一个关联值,可以使用通配符_表示。OC只能调用swift中Int类型的枚举。

    属性 & 函数

    enum中可以包含计算属性类型属性不能包含存储属性

    enum Direct: Int {
        case up
        case down
        case left
        case right
        
        // 计算型属性
        var description: String{
            switch self {
            case .up:
                return "这是上面"
            default:
                return "这是\(self)"
            }
        }
        
        //存储属性:编译器报错,Enums must not contain stored properties
        //var radius: Double
        
        //类型属性 - 是一个全局变量
        static let height = 20.0
        
        // 函数
        func printSelf() {
            print(description)
        }
        
        mutating func nextDay(){
            if self == .up{
                self = Direct(rawValue: 1)!
            }else{
                self = Direct(rawValue: self.rawValue+1)!
            }
        }
    }
    
    Direct.down.printSelf()
    //打印结果:这是down
    
    var direct = Direct.left;
    direct.nextDay();
    direct.printSelf()
    //打印结果:这是right
    

    为什么struct中可以放存储属性,而enum不可以?
    struct中可以包含存储属性,是因为其大小就是存储属性的大小。而enum是不一样的(请查阅后文的enum大小讲解),enum枚举的大小是取决于case的个数的,如果没有超过255,enum的大小就是1字节(8位)

    可以在enum中定义实例方法static修饰的方法

    enum Week: Int{
        case MON, TUE, WED, THU, FRI, SAT, SUN
        
        mutating func nextDay(){
            if self == .SUN{
                self = Week(rawValue: 0)!
            }else{
                self = Week(rawValue: self.rawValue+1)!
            }
        }
    }
    
    <!--使用-->
    var w = Week.MON
    w.nextDay()
    print(w)
    

    三、枚举的嵌套

    枚举的嵌套主要用于以下场景:
    1、枚举嵌套枚举:一个复杂枚举是由一个或多个枚举组成;
    2、结构体嵌套枚举:enum是不对外公开的,即是私有的。

    3.1 enum嵌套enum

    枚举嵌套枚举,改动上面的例子:

    enum CombineDirect{
        //枚举中嵌套的枚举
        enum BaseDirect{
            case up
            case down
            case left
            case right
        }
        //通过内部枚举组合的枚举值
        case leftUp(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
        case leftDown(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
        case rightUp(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
        case rightDown(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
    }
    
    //使用
    let leftUp = CombineDirect.leftUp(baseDIrect1: CombineDirect.BaseDirect.left, baseDirect2: CombineDirect.BaseDirect.up)
    
    结构体嵌套枚举
    //结构体嵌套枚举
    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
        
        let key: KeyType
        
        func launchSkill(){
            switch key {
            case .left, .right:
                print("left, right")
            case .up, .down:
                print("up, down")
            }
        }
    }
    
    枚举的递归:indirect

    递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect 来表示该成员可递归。

    //一、用枚举表示链表结构
    enum List<T>{
        case end
        //表示case使是引用来存储
        indirect case node(T, next: List<T>)
    }
    
    
    //二、也可以将indirect放在enum前
    //表示整个enum是用引用来存储
    indirect enum List<T>{
        case end
        case node(T, next: List<T>)
    }
    

    第一种写法,如果没有关键字indirect,编译报错。原因是使用该枚举时,enum的大小需要case来确定,而case的大小又需要使用到enum大小。所以无法计算enmu的大小,于是报错!
    根据编译器提示,需要使用关键字indirect,意思就是将该枚举标记位递归,同时也支持标记单个case。
    enum内存大小:

    enum List<T>{
        case end
        indirect case node(T, next: List<T>)
    }
    
    print(MemoryLayout<Int>.size)
    print(MemoryLayout<List<Int>>.size)
    print(MemoryLayout<List<Int>>.stride)
    
    print(MemoryLayout<String>.size)
    print(MemoryLayout<List<String>>.size)
    print(MemoryLayout<List<String>>.stride)
    
    //打印结果:
    //8  8  8
    //16  8  8
    

    发现IntString都是8。为什么?
    下面通过LLDB分析查看一下:

    image.png
    如果是end,此时存储的是case值,为0,而case为node时存储的是引用地址
    所以,indirect关键字其实就是通知编译器,我当前的enum是递归的,大小是不确定的,需要分配一块堆区的内存空间,用来存放enum。

    四、swift和OC混编enum

    在swift中,enum非常强大,而在OC中,enum仅仅只是一个整数值
    因此,OC调用Swift枚举,必须具备2个条件:①、用@objc关键字标记enum;②、当前enum应该是Int类型

    // Swift中定义枚举
    @objc enum Weak: Int{
        case MON, TUE, WED, THU, FRI, SAT, SUN
    }
    
    // OC使用
    - (void)test{
        Weak mon = WeakMON;
    }
    

    Swift使用OC枚举,OC中的枚举会自动转换成swift中的enum

    // OC定义1:NS_ENUM
    NS_ENUM(NSInteger, ENUM_OC_TYPE){
        Value1,
        Value2
    };
    
    // OC定义2:typedef enum
    typedef enum {
        Num1,
        Num2
    }OCEnumType;
    
    // swift使用
    //1、将OC头文件导入桥接文件xxx-Bridging-Header.h
    #import "xxx.h"
    //2、使用
    let ocEnum1 = ENUM_OC_TYPE.Value1
    let ocEnum2 = OCEnumType.init(0)
    
    print("\(ocEnum1)  +  \(ocEnum2.rawValue)")
    //打印结果:ENUM_OC_TYPE  +  0
    
    OC自动转换成swift方式: image.png image.png
    上图可知,通过typedef enum定义的enum,在swift中变成了一个结构体,并遵循了两个协议:EquatableRawRepresentable
    OC使用Swift中String类型的枚举方式
    @objc enum Weak: Int{
        case MON, TUE, WED
        
        var val: String?{
            switch self {
            case .MON:
                return "MON"
            case .TUE:
                return "TUE"
            case .WED:
                return "WED"
            default:
                return nil
            }
        }
    }
    
    // OC中使用
    Weak mon = WeakMON;
    
    // swift中使用
    let weak = Weak.MON.val
    

    即:swift中的enum成Int整型;enum再声明一个变量/方法,用于返回固定的字符串,用于在swift中使用。

    五、Enum内存大小

    我们主要分析两个函数的区别:

    1. size:实际占用内存大小;
    2. stride:系统分配的内存大小。
    5.1 普通enum

    一个case的情况:

    enum Weak {
        case MON
        //case TUE
    }
    
    print(MemoryLayout<Weak>.size)
    print(MemoryLayout<Weak>.stride)
    //打印结果:0  1
    

    再加一个case:

    enum Weak {
        case MON
        case TUE
    }
    
    print(MemoryLayout<Weak>.size)
    print(MemoryLayout<Weak>.stride)
    //打印结果:1  1
    

    继续增加多个case:

    enum Weak {
        case MON
        case TUE
        case WED
        case THU
        case FRI
        case SAT
        case SUN
    }
    
    print(MemoryLayout<Weak>.size)
    print(MemoryLayout<Weak>.stride)
    //打印结果:1  1
    

    以上可以看出,当case个数为1时,枚举size为0个数>=2时,size的大小始终是1,即,说明enum就是以1字节存储在内存中的。why?

    image.png
    读取内存可以看出,case都是1字节大小,1个字节是8个byte,那么有255种排列组合(0x00000000 - 0x11111111)。
    所以,当case为1个的时候,size的大小是0(二进制是0x0)case数<=255时,size都是1,是UInt8类型
    超过255个时,会自动扩容,size和stride都会增加

    总结:
    1、如果enum中有原始值,即rawValue,其大小取决于case的多少,如果没有超过UInt8即255,则就是1字节存储case,Int标识的其实就是 RawValue的值。
    2、当只有一个case的情况下,size是0,表示这个enum是没有意义的。
    3、当有两个及以上case时,如果没有超过255,则case的步长是1字节;如果超过,则UInt8->UInt16...,以此类推。

    5.2 关联值的enum

    自定义类型的枚举,即关联值类型,sizestride值的变化,如下:

    enum Shape{
        case circle(radius: Double)
        case rectangle(width: Double, height: Double)
    }
    print(MemoryLayout<Shape>.size)
    print(MemoryLayout<Shape>.stride)
    //打印结果:17   24
    

    分析:

    1. case(枚举值)size1
    2. 枚举是共用内存,有关联值时,取最大值
      2.1 circle的参数是double类型,内存size8
      2.2 rectangle的参数是两个double类型,内存size16
    3. 最大值,为16。即,内存size为:16(最大关联值大小) + 1(case枚举值)= 17
    4. 内存对齐,所以stride为24
      断点读取内存验证一下: image.png
    5.3 enum嵌套enum
    enum CombineDirect{
        enum BaseDirect{
            case up, down, left, right
        }
        
        case leftUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case rightUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case leftDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
        case rightDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
    }
    
    print(MemoryLayout<CombineDirect>.size)
    print(MemoryLayout<CombineDirect>.stride)
    //打印结果:2  2
    

    说明:enum嵌套enum同具有关联值的enum是一样的,同样取决于关联值的大小,其内存大小是最大关联值的大小
    定义一个变量,观察一下内存:

    var combine = CombineDirect.leftDown(baseDirect1: .left, baseDirect2: .down)
    
    image.png

    总结,有待进一步验证:
    enum嵌套enum同样取决于最大case的关联值大小;
    当嵌套enum的case只有2个时,case在内存中的存储是0、8;
    当嵌套enum的case大于2,小于等于4时,case在内存中的存储是 0、4、8、12;
    当嵌套enum的case大于4时,case在内存中的存储是从0、1、2...以此类推。

    5.4 结构体嵌套enum
    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    //打印结果:0   1
    

    如果只嵌套了enum,没有声明变量。size的大小取决于成员变量,但是struct中目前没有属性,所以size是1

    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
    
        let key: KeyType
    
        func launchSkill(){
            switch key {
            case .left, .right:
                print("left, right")
            case .up, .down:
                print("up, down")
            }
        }
    }
    
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    //打印结果:1  1
    

    结构体的大小计算,跟函数无关,所以只看成员变量key的大小,key是枚举Skill类型,大小为1,所以结构体大小为1
    如果在添加一个成员变量:

    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
        let key: KeyType //1字节
        var height: UInt8 //1字节
    
        func launchSkill(){
            switch key {
            case .left, .right:
                print("left, right")
            case .up, .down:
                print("up, down")
            }
        }
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    //打印结果:2   2
    

    在添加一个Int属性:

    struct Skill {
        enum KeyType{
            case up
            case down
            case left
            case right
        }
        let key: KeyType //1字节
        var height: UInt8 //1字节
        var width: Int //8字节
    
        func launchSkill(){
            switch key {
            case .left, .right:
                print("left, right")
            case .up, .down:
                print("up, down")
            }
        }
    }
    print(MemoryLayout<Skill>.size)
    print(MemoryLayout<Skill>.stride)
    //打印结果:16   16
    //打印结果2:如果把Int属性放在最前面,则为,10   16
    

    为什么属性位置不一样,打印结果不一样,也是因为内存对齐。内存对齐规则如下:

    /**
     数据成员的对齐规则可以理解为min(m, n) 的公式
     m,表示当前成员的开始位置;n,表示当前成员所需要的位数。
     如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储;
     反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。
     */
    struct Mystruct4{
        int a;              //4字节 min(0,4)--- (0,1,2,3)
        struct Mystruct5{   //从4开始,存储开始位置必须是最大的整数倍(最大成员为8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)满足,从8开始存储
            double b;       //8字节 min(8,8)  --- (8,9,10,11,12,13,14,15)
            short c;         //1字节,从16开始,min(16,1) -- (16,17)
        }Mystruct5;
    }Mystruct4;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            NSLog(@"%lu  %lu", sizeof(Mystruct4), sizeof(Mystruct4.Mystruct5));
            //打印结果:24  16
            
            NSLog(@"Hello, World!");
        }
        return 0;
    }
    

    结构体嵌套了enum总结:
    1、如果没有声明变量,此时的size是0,stride是1
    2、如果结构体中没有其他属性,只有枚举变量,那么结构体的大小就是枚举的大小,即size为1
    3、如果结构体中还有其他属性,则按照OC中的结构体内存对齐原则进行分析。

    内存对齐 & 字节对齐 区分
    内存对齐:iOS中是8字节对齐,苹果实际分配采用16字节对齐,这种只会在分配对象时出现。
    字节对齐:存储属性的位置必须是偶地址,即OC内存对齐中的min(m,n),其中m表示存储的位置n表示属性的大小,需要满足位置m整除n时,才能从该位置存放属性。简单来说,就是必须在自身的倍数位置开始。
    外部调用对象时,对象是服从内存对齐
    单纯从结构上说,结构内部服从最大字节对齐。即,枚举size为1的情况。

    总结:
    一、枚举定义:
    1、 enum中使用rawValue的本质是调用get方法,即在get方法中从Mach-O对应地址中取出字符串并返回的操作;
    2、 enuminit方法的调用是通过枚举.init(rawValue:)或者枚举(rawValue:)触发的;
    3、没有关联值的enum,如果希望获取所有枚举值,需要遵循CaseIterable协议,然后通过枚举名.allCase的方式获取;
    4、case枚举值rawValue原始值的关系:case 枚举值 = rawValue原始值
    5、具有关联值的枚举,可以称为三无enum,因为没有别名RawValue、init、计算属性rawValue
    6、enum模式匹配方式,主要有两种:switch-case / if-case
    7、enum可以嵌套enum,也可以在结构体中嵌套enum,表示该enum是struct私有的;
    8、enum中还可以包含计算属性类型属性,但是不能包含存储属性
    9、enum中可以定义实例static修饰的方法
    二、枚举内存:
    1、普通enum的内存大小一般是1字节,如果只有一个case,则为0,表示没有意义,如果case个数超过255,则枚举值的类型由UInt8->UInt16->UInt32...;
    2、具有关联值的enum大小,取决于最大case的内存大小+case的大小(1字节)
    3、enum嵌套enum同样取决于最大case的关联值大小
    4、结构体嵌套enum,如果没有属性,则size为0,如果只有enum属性size为1,如果还有其他属性,则按照OC中内存对齐原则进行计算。

    参考:枚举教程

    相关文章

      网友评论

        本文标题:Swift枚举

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