Swift-Singleton,无法全局访问

时间:2018-09-24 15:34:57

标签: swift xcode singleton

我想创建一个没有全局访问权限的Swift Singleton。我要创建的模式是确保始终仅存在一个类的一个实例,但是不应使用常规的全局MyClass.shared语法访问该类。这样做的原因是我希望该类可以完全正确地进行测试(这对于全局Singleton而言实际上是不可能的)。然后,我将使用依赖注入将单个实例从viewcontroller传递到viewcontroller。因此,无需全局静态实例即可解决“访问”问题。

我可以做的基本上是做任何事情。只需创建一个普通的类并信任所有开发人员的知识,就不会一次又一次实例化此类,而只能将其作为依赖项注入。但是我宁愿有一些禁止这种情况的编译器强制模式。

所以要求是:

  • 在编译期间确保仅实例化一个类的实例
  • 没有全局访问权限
  • 在单元测试期间不应强制执行仅实例化一个类的保证,因此可以正确地对其进行测试

我第一次尝试解决这个问题是这样的:

enter image description here

class WebService {
    private static var instances = 0

    init() {
        assertSingletonInstance()
    }

    private func assertSingletonInstance() {
        #if DEBUG
        if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false {
            WebService.instances += 1
            assert(WebService.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
        }
        #endif
    }
}

备注:在启动过程中传递参数会创建一个用户默认值,可以在运行时检查该默认值。这就是我知道当前运行的是单元测试的方式。

通常,此模式效果很好。我唯一的问题-我必须为每个可能的单例重复此代码。哪个不好我希望有一个可重用的解决方案。

单协议扩展

一种解决方案是创建协议扩展:

protocol Singleton {
    static var instances: Int { get set }
    func assertSingletonInstance()
}

extension Singleton {
    // Call this assertion in init() to check for multiple instances of one type.
    func assertSingletonInstance() {
        if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false {
            Self.instances += 1
            assert(Self.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
        }
        #endif
    }
}

然后以这种方式使用它:

class WebService: Singleton {)
    static var instances = 0

    init() {
        assertSingletonInstance()
    }
}

此方法的问题在于instances变量不是private。因此,有人可以在实例化类之前将该变量设置为0,并且检查将不再起作用。

单一基类

另一种尝试是Singleton基类。在这种情况下,可以使用private static var instances

class Singleton {
    private static var instances = 0

    required init() {
        assertSingletonInstance()
    }

    private func assertSingletonInstance() {
        #if DEBUG
            if UserDefaults.standard.bool(forKey: UserDefaultsKeys.isUnitTestRunning.rawValue) == false {
                Singleton.instances += 1
                assert(Singleton.instances == 1, "Do not create multiple instances of this class. Get it thru the shared dependencies in your module.")
            }
        #endif
    }
} 

此方法的问题是-它不起作用。递增Singleton.instance会将1添加到static instances类型的Singleton中,而不是添加到从Singleton基类派生的类中。

现在,我要么什么都不做,要么依靠所有开发人员的纪律和理解,或者至少使用具有internalpublic访问权限的协议扩展。

可以找到here的示例实现。

也许有人对于更好地解决此问题有更好的主意。我感谢任何提示或讨论。谢谢。

2 个答案:

答案 0 :(得分:2)

您可以使用原子标记(出于线程安全)将单例标记为已实例化:

class Singleton {

    static private var hasInstance = atomic_flag()

    init() {
        // use precondition() instead of assert() if you want the crashes to happen in Release builds too
        assert(!atomic_flag_test_and_set(&type(of: self).hasInstance), "Singleton here, don't instantiate me more than once!!!")
    }

    deinit {
        atomic_flag_clear(&type(of: self).hasInstance)
    }
}

您将单例标记为在init中分配,然后在deinit中将标志重置。这样一来,您可以只拥有一个实例(如果原始实例没有被解除分配),另一方面,您可以拥有多个实例,只要它们不重叠即可。

应用程序代码:假设您在某个位置保留了对下游注入的单例的引用,那么永远不要调用deinit,这只会导致一种可能的分配。

单元测试代码:如果单元测试正确进行了清理(每次测试后被释放的单例都被释放了),那么在某个时间点将只有一个活动实例,这将赢得不会触发断言失败。

答案 1 :(得分:0)

回应Cristik的回答:

这是一个非常好的解决方案! type(of: self)解决了基类问题。并且在deinit中释放事物是一个好主意,允许整个事物进行单元测试。您是对的-我将所有Singletons的引用保留在“上游”,然后再注入它们。完善。

我已经基于这个想法创建了一个带有串行队列的模板,用于解决可能的竞争状况问题。我认为这是一个比atomic_flag和“ Swiftish”更好的解决方案。

游乐场代码:

import Foundation

class Singleton {
    static private var instances = 0

    // Sync the access to instances
    private var serialQueue = DispatchQueue(label: "com.yourcompany.app.singletoncheck")

    init() {
        serialQueue.sync {
            type(of: self).instances += 1
            assert(type(of: self).instances == 1, "Do not create multiple instances of this class living at the same time.")
        }
    }

    deinit {
        type(of: self).instances = 0
    }
}

class Derived: Singleton {

}

var a: Derived? = Derived()
//a = nil // release to prevent the assertion from failing
var b: Derived? = Derived() // assertion fails here, works!

这是一个更有趣的解决方案,可以在没有任何专门知识和声明的情况下在任何地方使用。它使用了失败的初始化程序。

游乐场代码:

import Foundation

class Singleton {
    static private var instances = 0

    // Sync the access to instances
    let serialQueue = DispatchQueue(label: "com.yourcompany.app.singletoncheck")

    // This failable initializer assures that at the same time only one instance of this class exists.
    init?() {
        var singleInstance = false
        serialQueue.sync {
            type(of: self).instances += 1
            if type(of: self).instances == 1 {
                singleInstance = true
            }
        }
        if !singleInstance {
            return nil
        }
    }

    deinit {
        serialQueue.sync {
            type(of: self).instances = 0
        }
    }
}

class Derived: Singleton {
    var a = 0
    func increment() {
        serialQueue.sync {
            a += 1
            print(a)
        }
    }
}

var a = Derived()
a?.increment() // call to synchonized version of increment
//a = nil //either a or b is alive
var b = Derived()

print (a) //prints Optional(__lldb_expr_15.Derived)
print (b) //prints nil

在我看来,这是由四个人组成的“真实”辛格尔顿。当时,全局访问只是实现细节。

因此,与通常的Singleton模式相比:

  • 没有全局访问权限
  • 是可重复使用的模式
  • 所有共享的可变状态都可以同步(如果得到照顾)
  • 在单元测试中完全可测试

因此它具有Singleton的所有优点,但没有常见的问题。