dart null 安全的最佳实践

Best practices in dart null safety

我目前正在尝试提高我的 Flutter 应用程序中的空安全性,但在使用空安全方面的实际经验相对较少,我对自己的某些决定没有信心。

例如,我的应用程序需要用户登录,因此我有一个 Auth class 保留身份验证状态。

class Auth {
  final String? token;
  final User? user;

  Auth(this.token, this.user);
}

在我的应用程序中,我确保 user 属性 仅在用户登录时才被访问,因此执行以下操作是安全的:

final auth = Auth(some_token, some_user);

// and when I need to access the user

final user = auth.user!

引出第一个问题:

是否建议在应用中多处使用空断言运算符?

我个人觉得在整个应用程序中做 auth.user!.id 之类的事情有点不舒服,所以我目前是这样处理的:

class Auth {
  final User? _user;

  Auth(this._token, this._user);

  User get user {
    if (_user == null) {
      throw StateError('Invalid user access');
    } else {
      return _user!;
    }
  } 
}

但我不确定这是否是空安全的推荐做法。


对于下一个问题,我有一个处理 API 调用的 class:

class ApiCaller {
  final String token;
  
  ApiCaller(this.token);
  
  Future<Data> getDataFromBackend() async {
    // some code that requires the token
  }
}

// and is accessed through riverpod provider
final apiCallerProvider = Provider<ApiCaller>((ref) {
  final auth = ref.watch(authProvider);
  return ApiCaller(auth.token);
})

我的 ApiCaller 是通过提供程序访问的,因此该对象是在应用程序启动时创建的。显然,它需要 token 可用,因此取决于 Auth。但是,当应用程序启动且用户未登录时,token 也可能是 null

因为我确信 apiCaller 在没有现有用户时不会被使用,所以这样做:

class ApiCaller {
  // make token nullable
  final String? token;
  
  ApiCaller(this.token);
  
  Future<Data> getDataFromBackend() async {
    // some code that requires the token
    // and use token! in all methods that need it
  }
}

final apiCallerProvider = Provider<ApiCaller>((ref) {
  final auth = ref.watch(authProvider);
  if (auth.token == null) {
    return ApiCaller()
  } else {
    return ApiCaller(auth.token);
  }
})

应该没问题。但是,这也让我在所有方法中都使用了很多 token!,对此我不太确定。

我也可以简单地在非空 token 版本中执行 ApiCaller(''),但这似乎更像是一种解决方法,而不是一种好的做法。


很抱歉提出冗长的问题。我试着寻找一些更好的关于空安全的现实世界实践的文章,但大多数只是语言基础知识,所以我希望你们中的一些人能给我一些见解。提前致谢!

当您知道可空变量不为空时避免使用 ! 的最简单方法是像您在第一个问题中所做的那样 non-null getter:

User get user {
    if (_user == null) {
      throw StateError('Invalid user access');
    } else {
      return _user!;
    }
  } 

我会让你知道在抛出错误之前不需要检查值是否为空,空检查运算符就是这样做的:

Uset get user => _user!;

当然,除非您非常关心错误本身并想抛出其他错误。

至于你的第二个问题,那个有点棘手,你知道你不会不会在变量初始化之前访问它,但是你必须在它有之前初始化它一个值,因此您唯一的选择是将其设为 null,我个人不喜欢使用 late 关键字,但它是专门为此目的构建的,因此您可以使用它。迟到的变量在明确赋值之前不会有值,否则会抛出错误,另一种解决方案是像另一页上那样制作 non-null getter。

另外,这里不需要空检查,因为结果是一样的:

if (auth.token == null) {
    return ApiCaller()
  } else {
    return ApiCaller(auth.token);
  }

改为这样做:

return ApiCaller(auth.token);

这对我来说确实是一个简单的问题,你只是不习惯使用 null-safety,这意味着对你来说,! 看起来很丑陋或不安全,但你越是使用它的次数越多,您就会越熟悉它,即使您在您的应用程序中经常使用它,它看起来也不会像糟糕的代码。

希望我的回答对你有帮助

Is it recommended to use the null assertion operator in many places in the app?

我认为 null 断言运算符有点代码味道,尽可能避免使用它。在许多情况下,it can be avoided by using a local variable, checking for null, and allowing type promotion to occur 或使用 null-aware 运算符来实现优雅的失败。

在某些情况下,使用 null 断言 更简单、更清晰,只要您可以从逻辑上保证该值不会是 null。如果您认为您的应用程序因失败的 null 断言而崩溃,因为这在逻辑上是不可能的,那么使用它就完全没问题。

I personally find it kind of uncomfortable to do things like auth.user!.id throughout the app, so I'm currently handling it like this:

class Auth {
  final User? _user;

  Auth(this._token, this._user);

  User get user {
    if (_user == null) {
      throw StateError('Invalid user access');
    } else {
      return _user!;
    }
  } 
}

but I'm not sure if this is a recommended practice in null-safety.

除非你想控制错误,否则抛出 StateError 是没有意义的。空断言运算符无论如何都会抛出一个错误(TypeError)。

我个人认为 user getter 没有太大价值。您仍然会在任何地方使用 null 断言运算符,但它只是隐藏在方法调用后面。它会使代码更漂亮,但不太清楚潜在的故障点在哪里。

如果您发现自己在一个函数中多次对同一个变量使用 null 断言运算符,您仍然可以使用局部变量来使它更好:

void printUserDetails(Auth auth) {
  final user = auth.user;
  user!;

  // `user` is now automatically promoted to a non-nullable `User`.
  print(user.id);
  print(user.name);
  print(user.emailAddress);
}

我认为最终你需要决定你想要你的 public API 是什么以及它的合同是什么。例如,如果用户未登录,那么拥有一个 Auth 对象是否有意义?您能否让 Auth 使用 non-nullable 成员,并让消费者使用 Auth? 而不是 Auth,其中 null 表示“未登录”?虽然这会将责任推给调用者,让他们到处检查 null,但他们已经负责在未登录时不做任何访问 Auth.user 的事情。

另一个 API 考虑因素是您希望故障模式是什么。您的 API 合同是否明确规定呼叫者在未登录时不得访问 Auth.user?如果来电者有疑问,他们是否能够检查自己?如果是这样,那么在 null 时对 Auth.user 进行致命访问是合理的:调用者由于逻辑错误而违反了合同,应该更正。

但是,在某些情况下,这可能太苛刻了。无论如何,您的操作可能会因其他原因在运行时失败。在这些情况下,您可以考虑优雅地失败,例如向调用者返回 null 或一些错误代码。

final apiCallerProvider = Provider<ApiCaller>((ref) {
  final auth = ref.watch(authProvider);
  if (auth.token == null) {
    return ApiCaller()
  } else {
    return ApiCaller(auth.token);
  }
}

您的 ApiCaller class 没有 zero-argument 构造函数,所以这没有意义。如果你的意思是它的构造函数是:

final String? token;

ApiCaller([this.token]);

那么ApiCaller()ApiCaller(null)就没有区别了,你还不如无条件的用ApiCaller(auth.token).