it-swarm-ja.com

変換関数がnullableを取り込んでnullableを返すか、呼び出し元がnullabilityを処理するか

多くの場合、ネットワークモデルをデータモデルに、またはデータモデルをバイナリ表現に、タイプを別のタイプに変換する必要があります。これらの変換関数はOptional/nullable値を取り、nilの場合はすぐにnilを返す必要がありますか、それとも関数は非nil値のみを受け入れ、常に非nil変換を返す必要がありますか?データを受信するときに私のコードが検証を適切に処理すると仮定します。そのため、それ以上の検証は必要なく、変換のみが必要です。

例1:呼び出し元にnilオブジェクトを処理させる

func makeUser(from apiUser: APIUser) -> User {
    return User(id: apiUser.id)
}

var user: User? = nil
func downloadUser() {
    service.getUser { (apiUser: APIUser?) in
        if let apiUser = apiUser {
            self.user = makeUser(from: apiUser)
        } else {
            self.user = nil
        }
    }
}

例2:変換でnilオブジェクトを処理する

func makeUser(from apiUser: APIUser?) -> User? {
    if apiUser == nil {
        return nil
    }
    return User(id: apiUser.id)
}

var user: User? = nil
func downloadUser() {
    service.getUser { (apiUser: APIUser?) in
        self.user = makeUser(from: apiUser)
    }
}

例1は、APIUserが常にUserオブジェクトに変換されることを明示的に示しています。例2は、保証されていても、有効なAPIUserが有効なUserオブジェクトにマップされない可能性があることを暗黙的に示しています。

例2は、共通ロジックが必要になるたびにコピーされるのではなく、変換で処理されることを示していますが、関数コントラクトで検証がすでに行われているにもかかわらず、変換がオプション/ null可能値を返すことができるという事実は個人的には嫌です実行され、有効な値が失敗することはありません。

3

オプションのパラメーターの唯一の目的がパラメーターがnilの場合にnilを返すことである場合、私はそれをしません。パラメーターを非オプションにして、追加のタイプセーフを取得します。その後、nilの可能性のある値を持っている人は、それをチェックできます。多くの場合、値は実際にはnilにならず、最後にチェックコードがないことが判明します。

1
gnasher729

呼び出し元はnilを異なる方法で処理したい場合があります

Optionalの威力は、ユーザーがnilをさまざまな方法で処理できるようにすることです。

  1. Nil-coalescence演算子(??)を使用してデフォルト値を置き換えることができます
  2. 強制アンラップ演算子(!)を使用して、nilではないと断言できます。
  3. mapまたはflatMapを使用して値を変換できます。

普遍的に正しいアプローチはありません。したがって、呼び出し先側の処理で行う決定は、少なくとも一部の呼び出し元では間違っている可能性があります。そのため、呼び出し元にnilを自分で処理させる必要があります。

それは完全に混乱するでしょう

これを実際に考えると、「呼び出し先は呼び出し元のnilを処理する必要がある」と判断された場合、すべてのSwiftコードはオプションでいっぱいに散らかされます。それはそれほど素晴らしいことではありません。

この仕事のためのツールはすでにあります

xnilであるかどうかを確認し、そうでない場合はfを介して渡して変換し、そうでない場合はnilを静かに渡してください」と同じコードを何度も繰り返すことがわかります。このようなコードで繰り返しを特定する場合、ある種の抽象化を使用してそれを抽出することを目的とする必要があります。この場合、Optional.map(_:)の形式ですでに存在しています。

次の2つの関数があるとします。

func makeUserHandlingOptionals(from apiUser: APIUser?) -> User? { ... }
func makeUser(from apiUser: APIUser) -> User { ... }

1つは他の観点から簡単に実装できます。

func makeUserHandlingOptionals(from apiUser: APIUser?) -> User? {
   return apiUser.map(makeUser(from:))
}

実際、非常に簡単なので、makeUserHandlingOptionals(from:)を宣言する必要はありません。呼び出し元がAPIUser?を変換する場合は、mapを自分で呼び出すようにします。

Javaでの同様の話

過去にも、同様の誘惑が起こりました。私はJava 1.8より前(ストリームAPIを導入)を書いていました。Aを取り、Bを返す関数がありました。しかし、ArrayList<A>があり、それを変換するのは非常に退屈でしたArrayList<B>に変換します。毎回自分で行う必要がありました。

B convertAtoB(A a) { ... }

ArrayList<A> inputOfAs = ...
ArrayList<B> outputOfBs = new ArrayList();

for (A inputA : inputOfAs) {
    outputOfBs.add(convertAtoB(inputA));
}

私はそれをたくさんする必要があり、それは面倒でした。私はあなたと同じような誘惑に駆られました。おそらくconvertAtoBの2番目のオーバーロードバージョンを導入する必要があります。これは、ArrayList<A>を受け取り、ArrayList<B>を返しました。しかし、それは厄介なように思えるので、おそらく正しいバージョンの選択は配列バージョンのみを持ち、単一の要素を変換する必要があるときに単一要素の配列リストを渡すことです。

最終的にストリームとmapのような「関数型スタイル」の構造について学ぶまで、私には良い解決策がありませんでした。 mapは、(A) -> B関数が([A]) -> [B]関数であるかのように機能できるようにする詳細を抽象化することにより、この問題を完全に解決します。そうすることで、convertAtoBは、配列リストをいじるのではなく、AからBへの変換に集中できます。

mapに切り替えると、パフォーマンスがさらに向上します。当時、ArrayList.addの呼び出しを繰り返すと、サイズ変更操作が時々発生することを理解していませんでした。現在の配列がいっぱいになると、ArrayListは新しい大きな配列を割り当て、O(n)時間を費やして古い要素をコピーする必要があります。しばらくしてからもう一度やり直してください。 mapの実装者は、入力配列と出力配列が常に同じサイズであることを知っています(mapの定義により)。したがって、サイズを変更したり、余分な要素を加えたりすることなく、すべての新しいB要素に適合するのに十分な大きさの出力配列を事前に割り当てることができます。それは当時私が考えていなかったものであり、その過程で「無料で」入手できました。

結論

TL; DR:関数は、担当する変換の実行に焦点を合わせたままにする必要があります。

MakeUser関数は値を受け取り、その値からユーザーを作成します。入力が無効なためにユーザーを作成できない場合は、例外をスローする必要があります。 「何も」を解析してユーザーを取得できないため、nullは間違いなく無効な入力です。

これは、intの解析に似ています。入力が無効な場合は、例外をスローし、うまくいったふりをしないでデフォルト値を返します。

これを行うと、makeUserを呼び出すたびに、nullではなく実際のUserオブジェクトが確実に存在することがわかります。

次に、downloadUserは、ダウンロードしたユーザーが有効であることを検証する必要があります(あなたのケースでは、それがnullではないことを確認しているようです)。

この1つの使用例では特に問題ではありませんが、makeUserへの他の呼び出しが機能する(この場合はユーザーオブジェクトがある)か、呼び出し元のコードで明示的に処理する必要があるという利点があります。これにより、アプリ全体でnull参照例外が発生する可能性を減らすことができます。

編集:要約すると、例1の方が私には適しています。

0
Jack

DTOであるAPIUserは、アプリケーションの境界に存在します。リモートサービスと対話するオブジェクトを超えてリークしてはなりません。どちらの例も適切なソリューションではありません。実際の問題は、serviceUserオブジェクトを返さず、APIUserからUserへのマッピングを内部で実行することにあります。

ヌル可能性は?それはリモートシステムです。あなたはそれを制御しません。 nullにすることもできます。

serviceは、User?を返すようにリファクタリングする必要があります。 APIがnull値を返す場合、servicenilを返す必要があります。ユーザーがリモートサービスに存在しない状況を処理する機会をアプリケーションに与える必要があります。これは、誰かがログインしようとした無実の間違いである可能性がありますが、彼らはまだユーザープロファイルを持っていません。 「このアプリケーションにアクセスする権限がありません」という丁寧なメッセージで十分であれば、アプリケーションをクラッシュさせる必要はありません。

だから基本的には答えは例3(下)です

// 'service' calls API and maps to User domain object
var user: User? = service.getUser()

if user == nil {
    // user not found in remote service, handle error
}

これは、アプリケーションのデータベースにクエリを実行することと正直に同じです。ユーザーが存在しない可能性があります。アプリケーションをクラッシュさせる代わりにnilを返しますが、例外はありますが、ユーザーが何らかの方法で処理する必要があります。

0
Greg Burghardt