"if #available"与不透明结果类型

2022-11-30 20:27:13 浏览数 (1)

介绍

SE-0360, Swift 5.7 已实现

自 SE-0244 引入以来,不透明的结果类型已成为类型级抽象的强大工具,允许库作者隐藏其API的实现细节。

根据 SE-0244 中描述的规则,返回不透明结果类型的函数必须从每个返回语句中返回与T类型相同的值,并且T必须满足不透明类型上所述的所有约束。

当前模型和实现限制了不透明结果类型作为抽象机制的有用性,因为它阻止了框架引入新类型并将其用作现有API 的基础类型。为了弥补这一可用性差距,本篇提议建议在可用条件下放宽对返回的同类型限制。

提议的动机

为了说明在不透明结果类型和可用性条件之间的交互问题,我们列举个框架例子,该框架下定义Shape协议,并且Square类型已经遵循该Shape协议,如下:

代码语言:Swift复制
protocol Shape {
  func draw(to: Surface)
}
 
struct Square : Shape {
  ...
}

在该库的新版本中,库作者决定引入一个新的shape - Rectangle, 但是新类型被限制了可用性,在新的版本需要通过@available来区分,例如:

代码语言:Swift复制
@available(macOS 100, *)
struct Rectangle : Shape {
  ...
}

因为RectangleSquare的变体,那么Square是可以转化为Rectangle。但是这种转化还是需要通过@available限制可用性,原因是因为Rectangle类型已经是受限制可用的。

代码语言:Swift复制
@available(macOS 100, *)
extension Square {
  func asRectangle() -> some Shape {
     return Rectangle(...)
  }
}

新方法asRectangle()必须在可用性上下文中声明才能返回Rectangle, 这限制了它的可用性,因为asRectangle()的所有用途都封装在#avaliable代码块中。

如果框架的老版本就存在asRectangle(),不声明if #available条件,就无法使用新类型:

代码语言:Swift复制
struct Square {
  func asRectangle() -> some Shape {
     if #available(macOS 100, *) {
        return Rectangle(...)
     }
     
     return self
  }
}

但上述这样声明是不允许的,因为asRectangle()函数主体中的所有返回语句都必须返回相同的具体类型。所以上述代码会报错:

代码语言:Swift复制
 error: function declares an opaque return type 'some Shape', but the return statements in its body do not have matching underlying types
  func asRectangle() -> some Shape {
       ^                ~~~~~~~~~~
note: return statement has underlying type 'Rectangle'
      return Rectangle()
             ^
note: return statement has underlying type 'Square'
    return Square()
           ^

对于库作者来说,这是一个死胡同,尽管SE-0244指出,在未来版本的库/框架中可能更改基础结果类型,但这个假设是基于该类型已经存在,因此可以在所有返回语句中使用。恰巧在实际情况下,新类型往往是后续版本中新增,所以所绕进了一个死胡同。

提议解决方案

为了弥补上述可用性不足,本篇提议:放宽带有if #available函数的同类型返回限制,如果if #available条件一定会被执行,那么它可以返回与函数其余部分返回类型不同的类型。不要求必须返回同一类型。

这个提议给函数带来 2 点改变:

  • 多个if #available可以根据它的动态性返回不同类型
  • 可以安全返回一个确定类型,不受可用性限制,即使不符合任何可用性条件

由于函数中的返回类型在函数未运行时就要确认,即在声明函数时就要确认函数的返回类型,但如果把可用性条件判断(if #available)和其他条件判断(比如,guard, if, switch)混合在一起,函数就无法确定返回类型, 并且需要在if #available中返回与函数其余部分相同的类型。(意思是:在if #available内部包含的所有if, guard, switch的每个return分支上,都需要返回相同类型)下面这个例子满足这些规则:

代码语言:Swift复制
func test() -> some Shape {
  if #available(macOS 100, *) { ✅
    return Rectangle()
  }
  
  return self
}

详细设计

无条件可用子句称为if #available子句。

if #available语句是满足下列条件的if或者else if语句:

  • 该子句是其所在函数顶层if语句的一部分
  • if条件语句之前,其所在函数还没有出现return语句
  • 子句的条件是一个#available条件
  • 该子句要么是初始的if子句,要么是紧随无条件可用性子句之后的其他if子句
  • 该子句至少包含一个return语句
  • 通过子句控制的所有结束路径都以return或者throw结束

if #available子句以外的所有返回语句必须返回彼此相同的类型,并且该类型必须与其所在函数一样可用。

所有在给定的if #available子句内的return语句必须每次返回相同的类型,这种类型必须与子句的#available条件一样可用。并且这个类型不必与子句之外的其他任何返回类型相同。

在所在函数中,必须至少有一种return语句。如果在无条件可用性子句之外,没有return语句,那么在if #available子句内部,至少存在一种返回类型与函数的返回返回相同。

函数的返回类型一般是:

  • 第一个无条件动态满足的if #available子句的return语句返回类型;
  • 所有的if #available子句之外的return语句返回类型;
  • 第一个if #available子句的return语句的返回类型,这个类型与函数返回类型一样

第一个示例是正确的,因为第一个if #available和第二个if #available都以return结束,代码如下:

代码语言:Swift复制
func test() -> some Shape {
  if #available(macOS 100, *) { ✅
    return Rectangle()
  } else if #available(macOS 99, *) { ✅
    return Square()
  }
  return self
}

但是如果把上述条件修改下:

代码语言:Swift复制
func test() -> some Shape {
  if cond {
    ...
  } else if #available(macOS 100, *) { ❌
    return Rectangle()
  }
  return self
}

编译器此时会报错,因为if #available和动态条件相关联。

如果if #available在可以return的动态条件之后,这种情况也是不允许的。比如在函数开始时,提前 guard并返回对象,那么这属于一个动态条件, 这种情况也会报错:

代码语言:Swift复制
func test() -> some Shape {
  guard let x = <opt-value> else {
    return ...
  }
    
  if #available(macOS 100, *) { ❌
    return Rectangle()
  }

  return self
}

同样,如果if #available出现在某个循环内,也会报错:

代码语言:Swift复制
func test() -> some Shape {
  for ... {
    if #available(macOS 100, *) { ❌
      return Rectangle()
    }
  }
  return self
}

下面例子中的 test() 方法符合上述规则,会通过编译器编译。该方法中if条件在它的分支内都有结果,且返回结果类型相同。if #available始终会通过return结果分支.

代码语言:Swift复制
func test() -> some Shape {
  if #available(macOS 100, *) {
     if cond { ✅
       return Rectangle(...)
     } else {
       return Rectangle(...)
     }
  }
  return self
}

但是如果test()函数中if #availableif分支返回不同类型,则编译器无法通过:

代码语言:Swift复制
func test() -> some Shape {
  if #available(macOS 100, *) {
     if cond { ❌
       return Rectangle()
     } else {
       return Square()
     }
  }
  return self
}

本篇提议的这种语义调整非常适合现有模型,因为它确保每个平台始终有一个通用的基础类型。

0 人点赞