Baidu Engineer's Guide to Avoiding Pitfalls in Mobile Development - Swift Language

picture

Author | Venus Group In the previous article, we introduced common memory leaks in mobile development, see " Guidelines for Baidu Engineers to Avoid Pitfalls in Mobile Development-Memory Leaks ". In this article, we will introduce some common problems in the Swift language.

For Swift developers, a big difference between Swift and OC is the introduction of optional types (Optional). Developers who are new to Swift can easily step on related codes.

In this issue, we bring several guidelines for avoiding pitfalls related to Swift optional types: optional types should be judged as empty; avoid using implicitly unwrapped optional types; rational use of Objective-C identifiers; careful use of mandatory type conversion. Hope it will be helpful for Swift developers.

1. The optional type (Optional) should be judged as empty

In Objective-C, you can use nil to indicate that the object is empty, but it is usually unsafe to use a nil object, and crashes or other abnormal problems may occur if you use it carelessly. In Swift, developers can use optional types to indicate whether a variable has a value or not, which can more clearly express whether the type is safe to use. If a variable may be empty, you can use ? to indicate it when declaring it, and it needs to be unpacked before use. For example:

var optionalString: String?

When using an optional type object, unpacking operation is required. There are two unpacking methods: mandatory unpacking and optional binding.

Mandatory unpacking using! to modify an optional object is equivalent to telling the compiler "I know this is an optional type, but here I can guarantee that it is not empty, please ignore the nullability check here when compiling", For example:

let unwrappedString: String = optionalString!  // 运行时报错:Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

Here! is used to force unpacking. If optionalString is nil, a runtime error will occur and a crash will occur. **Therefore, when using ! for forced unpacking, it must be ensured that the variable is not nil, and the variable must be judged as empty, **as follows:

if optionalString != nil {
    let unwrappedString = optionalString!
}

Compared with the insecurity of forced unpacking, generally speaking, another unpacking method is recommended, that is, optional binding. For example:

if let optionalString = optionalString {
    // 这里optionalString不为nil,是已经解包后的类型,可以直接使用
}

In summary, when unpacking optional types, you should try to avoid using forced unpacking, and use optional binding instead. If you must use forced unpacking, you must logically ensure that the type is not empty, and do a good job of annotation to increase the maintainability of subsequent code.

2. Avoid using Implicitly Unwrapped Optionals

Since the optional type needs to be explicitly unwrapped before each use, sometimes the variable will always have a value after the first assignment. If it is explicitly unwrapped every time it is used, it is cumbersome. Swift introduces the implicit Unpacking optional types, implicitly unpacking optional types can be represented by ! and can be used directly without explicit unpacking, for example:

var implicitlyUnwrappedOptionalString: String! = "implicitlyUnwrappedOptionalString"
var implicitlyString: String = implicitlyUnwrappedOptionalString

The implicit unwrapping of the above example will not cause problems during compilation and operation, but if a line of implicitlyUnwrappedOptionalString = nil is inserted between the two lines of code, a runtime error will occur and a crash will occur.

In our actual project, a module is usually maintained by multiple people. It is usually difficult to ensure that the variable is not nil after the first assignment or is only used after the first correct assignment. From a security point of view, when using the implicit solution The null operation is also performed before the package type, but this is no different from using the optional type. For optional types (?), using the compiler directly without unpacking will report an error, and for implicit unpacking types, it can be used directly, and the compiler cannot help us check whether it is empty. Therefore, in actual projects, it is not recommended to use implicitly unpacked optional types . If a variable is non-null, choose a non-null type, and if it cannot be guaranteed to be non-null, choose to use an optional type.

3. Reasonable use of Objective-C identifiers

Unlike Swift, OC is a dynamically typed language. For OC, there is no concept of optional, and it is impossible to check whether an object is nullable during compilation. Apple introduced a new Objective-C feature in Xcode 6.3: Nullability Annotations, which allows nonnull, nullable, null_unspecified and other identifiers to tell the compiler whether an object is nullable or non-nullable when coding. The meanings of each identifier are as follows:

nonnull, indicating that the object is non-empty, and has __nonnull and _Nonnull equivalent identifiers.

nullable, indicating that the object may be empty, with __nullable and _Nullable equivalent identifiers.

null_unspecified, does not know whether the object is empty, has __null_unspecified equivalent identifier.

The corresponding relationship between the object type marked by the OC identifier and the Swift type is as follows:

picture

In addition to the above identifiers, the header files created by Xcode are now wrapped by NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END by default, that is, the default identifiers of objects declared between them are nonnull.

In the scene where Swift and OC are mixed, the compiler will convert the OC object type to the Swift type according to the OC identifier. If there is no explicit identifier, the default is null_unspecified. For example:

@interface ExampleOCClass : NSObject
// 没有指定标识符,且没有被NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END包裹,标识符默认为null_unspecified
+ (ExampleOCClass *)getExampleObject; 
@end

@implementation ExampleOCClass
+ (ExampleOCClass *)getExampleObject {
    return nil; // OC代码直接返回nil
}
@end
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let _ = ExampleOCClass.getExampleObject().description // 报错:Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
    }
}

In the above example, the Swift code calls the OC interface to obtain an object, and the compiler implicitly converts the object returned by the OC interface into an implicitly unwrapped type for processing. Since the implicit unpacking type can be used directly without explicit unpacking, users often ignore that the OC returns the implicit unpacking type, and use it directly without passing the empty judgment. But when the code is executed, because the OC interface returns a nil, the unpacking of the Swift code fails, and a runtime error occurs.

In actual coding, it is recommended to explicitly specify that the OC object is nonnull or nullable . After modifying the above code, it is as follows:

@interface ExampleOCClass : NSObject
/// 获取可空的对象
+ (nullable ExampleOCClass *)getOptionalExampleObject;
/// 获取不可空的对象
+ (nonnull ExampleOCClass *)getNonOptionalExampleObject;
@end

@implementation ExampleOCClass
+ (ExampleOCClass *)getOptionalExampleObject {
    return nil;
}
+ (ExampleOCClass *)getNonOptionalExampleObject {
    return [[ExampleOCClass alloc] init];
}
@end
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // 标注nullable后,编译器调用接口时,会强制加上 ?
        let _ = ExampleOCClass.getOptionalExampleObject()?.description 
        // 标注nonnull后,编译器将会把接口返回当做不可空来处理
        let _ = ExampleOCClass.getNonOptionalExampleObject().description 
    }
}

After adding nonnull or nullable identifiers to OC objects, it is equivalent to adding Swift-like "statically typed language features" to OC codes, so that the compiler can perform nullable type detection on the code, effectively reducing the crash when mixing risks of. But this "static feature" is not fully effective for OC. For example, the following code, although the declared return type is nonnull, can still return nil:

@implementation ExampleOCClass
+ (nonnull ExampleOCClass *)getNonOptionalExampleObject {
    return nil; // 接口声明不可空,但实际上返回一个空对象,可以通过编译,如果Swift当作非空对象使用,则会发生崩溃
}
@end
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        ExampleOCClass.getNonOptionalExampleObject().description
    }
}

Based on the above example, a runtime error will still be generated. From a security point of view, it seems that Swift is best to perform empty judgment when using all OC interfaces. But in fact, this will cause Swift code to be filled with a lot of redundant null-judgment codes, which greatly reduces the maintainability of the code, and also violates the coding principle of "exposing problems, not hiding them". This is not recommended. A reasonable approach is to do a security check on the OC side. The OC should check the return type to ensure the correctness of the return type and the correspondence between the return value and the identifier.

On the whole, it is best to follow the following usage principles for OC-side identifiers:

1. It is not recommended to use NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END, because the default modifier is nonnull, and it is easy to ignore whether the returned object is empty in actual development. Returning empty will result in a Swift runtime error. It is recommended that all OC interfaces involved in mixing must be explicitly decorated with corresponding identifiers.

2. The OC interface should be carefully decorated with nonnull. It must be used when the return value cannot be empty. Any interface that cannot be determined to be nullable needs to be marked as nullable.

3. In order to avoid unnecessary type and null checks on the Swift side (violating the Swift design concept), in an ideal state, type checks need to be performed on the OC side to ensure that the returned objects and marked identifiers are completely correct, so that Swift Then you can fully rely on the object type returned by OC.

4. When Swift calls OC code, pay attention to the type returned by OC, especially when returning an implicit unwrapped type, it must be null-judged.

5. Before the OC code supports Swift calls, check the return type and identifier of the OC code in advance to ensure that the object returned to Swift is safe.

4. Use mandatory type conversion with caution

GEEK TALK

As a strongly typed language, Swift prohibits all default type conversions, which requires coders to clearly define the type of each variable, and must explicitly perform type conversions when type conversions are required. Swift can perform type conversions using the as and as? operators.

The as operator is used for forced type conversion. In the case of type compatibility, one type can be converted to another type, for example:

var d = 3.0 // 默认推断为 Double 类型
var f: Float = 1.0 // 显式指定为 Float 类型
d = f // 编译器将报错“Cannot assign value of type 'Float' to type 'Double'”  
d = f as Double // 需要将Float类型转换为Double类型,才能赋值给f

In addition to the basic types listed above, Swift is also compatible with the conversion between basic types and corresponding OC types, such as NSArray/Array, NSString/String, NSDictionary/Dictionary.

If the type conversion fails, a runtime error will result. For example:

let string: Any = "string"
let array = string as Array // 运行时错误

The string variable here is actually a String type, trying to convert the String type to the Array type will cause a runtime error.

Another way to convert types is to use the as? operator, which returns an optional of the converted type if the conversion succeeds, or nil if the conversion fails. For example:

let string: Any = "string"
let array = string as? Array // 转换失败,不会产生运行时错误

Here, since the String type cannot be converted to the Array type, the conversion fails, and the value of the array variable is nil, but no runtime error is generated.

On the whole, when performing type conversion, you need to pay attention to the following points:

1. Type conversion can only be performed between compatible types. For example, Double and Float can be converted to each other, but String and Array cannot be converted to each other.

2. If you use as to perform mandatory type conversion, you need to ensure that the conversion is safe, otherwise it will cause a runtime error. If the conversion types cannot be guaranteed to be compatible, the as? operator should be used. For example, when parsing network data into model data, the type of network data cannot be guaranteed, and as? should be used.

3. When using the as? operator for type conversion, you need to pay attention to the situation that the return value may be nil.

----------  END  ----------

Recommended reading [Technical Gas Station] series:

Baidu Engineer's Guide to Avoiding Pitfalls in Mobile Development - Memory Leaks

Baidu Programmer Development Pit Avoidance Guide (Go Language)

Baidu Programmer Development Pit Avoidance Guide (3)

Baidu Programmer Development Pit Avoidance Guide (Mobile Terminal)

Baidu Programmer Development Pit Avoidance Guide (Front End)

picture

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4939618/blog/8900967