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)
.
我目前正在尝试提高我的 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)
.