レコードスプレッド演算子

異なるモデルの種類をマージせずにフィールドを共有するには、F# 11 以降で使用できるレコード スプレッドを使用します。

...Typeを使用して、新しい名前付きレコードにフィールド宣言を投稿するか、フィールド値を名前付きレコードまたは匿名レコードに提供する...expressionを使用します。 宣言の再利用と値のマッピングは、独立した選択肢です。

レコードの種類を作成する

構成の種類間で接続フィールドを共有するには、 Endpoint を新しいレコード宣言に分散します。

type Endpoint = { Host: string; Port: int }

type ClientOptions =
    { ...Endpoint
      TimeoutSeconds: int }

ClientOptions には、 Host、 Port、および TimeoutSecondsがあります。 これは、 Endpoint サブタイプではなく、個別の名目型です。

同じ型定義では、複数のソースと明示的な宣言を使用できます。

type Logging = { LogLevel: string }
type LoggedClientOptions = { ...ClientOptions; ...Logging }

レコード フィールドのみが参加します。 メソッド、レコード フィールドではないプロパティ、および静的メンバーはコピーされません。 ソースには、名前付きレコードまたは匿名レコードの種類 (匿名レコードの省略形を含む) を指定できます。 型スプレッドの変換先は、名前付きレコード型である必要があります。

レコード値を作成する

エンドポイントをクライアント オプションにコピーし、匿名レコードを使用してホストを置き換えます。

let local = { Host = "localhost"; Port = 5432 }
let options: ClientOptions = { ...local; TimeoutSeconds = 30 }

let remote = {| ...options; Host = "db.example.com" |}

printn $"{remote.Host}:{remote.Port}, timeout {remote.TimeoutSeconds}s"
// db.example.com:5432, timeout 30s

式スプレッドでは、構造体レコードを含む、名前付きまたは匿名のレコード ソースと宛先を受け入れます。 各宛先フィールドは値を受け取る必要があります。 明示的なターゲット注釈は、複数の名前付きレコードの種類がラベルを共有する場合に便利です。

フィールドの種類を強化する

後のフィールド宣言は、スプレッド フィールドの型を置き換えることができます。 たとえば、 type CustomerId = CustomerId of System.Guidで解析された識別子を表し、その識別子を持つレコードとインポートされたレコードを区別します。

type ImportedCustomer = { Name: string; Id: string }
type IdentifiedCustomer = { ...ImportedCustomer; Id: CustomerId }

let identify (source: ImportedCustomer) =
    match Guid.TryParse source.Id with
    | true, id -> Ok ({ ...source; Id = CustomerId id }: IdentifiedCustomer)
    | false, _ -> Error "Invalid customer identifier."

Guid.TryParse テキストを検証します。スプレッドはフィールドのみをコピーします。 CustomerId は、それ以上の規則を適用することなく、識別子を他の GUID と区別します。

独立したモデルを結合する

キュレーションされた名目レコードと匿名レコードの種類から宣言を作成し、独立したソースから値をアセンブルします。

type PublicProductFields = { Name: string; UnitPrice: decimal }
type ProductRow = { ...PublicProductFields; InternalCost: decimal }
type StockCounts = {| InStock: int; Reserved: int |}

type ProductCard =
    { ...PublicProductFields
      ...StockCounts
      CanOrder: bool }
let product: ProductRow =
    { Name = "Notebook"; UnitPrice = 12.50m; InternalCost = 4m }
let stock: StockCounts = {| InStock = 8; Reserved = 2 |}

let card: ProductCard =
    { ...product; ...stock; CanOrder = stock.InStock > stock.Reserved }

let viewState = {| ...card; Selected = false |}
let selected = {| ...viewState; Selected = true |}

名前付きソースと匿名ソースは ProductCardを提供し、カードは匿名ビューステートを提供します。 CanOrder は 1 回計算され、後の構築でカウントが変更されたときに再計算されません。

既知のターゲットにProjectする

ターゲットの種類は、保持するフィールドを選択します。 複数のソースで代替品を提供できます。

let endpoint: Endpoint = { ...options }
let overrides = {| Host = "db.example.com"; Port = 5433 |}
let production: ClientOptions =
    { ...options; ...overrides; TimeoutSeconds = 60 }

printn $"{endpoint.Host}:{endpoint.Port}"
printn $"{production.Host}:{production.Port}, timeout {production.TimeoutSeconds}s"
// localhost:5432
// db.example.com:5433, timeout 60s

endpoint では、 Host と Portのみが保持されます。 production では、ソースと明示的なオーバーライドが結合されます。 ターゲットに存在しない明示的なフィールドは、エラーのままです。

未示の匿名レコードは、すべての提供フィールドを受け取ります。 名前付きターゲットは推論可能である必要があります。 不明なソースには、関数パラメーターの (source: ClientOptions) など、独自の注釈が必要です。

フィールドの順序とオーバーライド

投稿は左から右に読み取られます。 フィールド名を繰り返す場合、 右端のコントリビューションが優先されます。 この規則は、型のフィールド宣言と式のフィールド値の両方に適用されます。

let laterHost = {| Host = "db.example.com" |}
let corrected: Endpoint = { ...local; ...laterHost; Port = 5433 }

型のオーバーライドの後に、置換型の値を指定します。 { Host = "first"; Host = "second" }などの明示的なフィールドを繰り返しても、エラーが残ります。

型宣言の場合、当選したフィールドが当選貢献の位置に表示されます。 ソースのフィールドは、ソースの順序を保持します。 この順序は、コンストラクターとシグネチャの準拠に重要です。

評価とコピー

拡散ソース式は、後のコントリビューションによって指定されたすべてのフィールドが置き換えられた場合でも、ソース順にそれぞれ 1 回実行されます。

let visits = ResizeArray<string>()
let hostFrom source host =
    visits.Add source
    {| Host = host |}

let selected =
    {| ...hostFrom "first" "localhost"
       ...hostFrom "second" "db.example.com" |}

assert (selected.Host = "db.example.com")
assert (Seq.toList visits = [ "first"; "second" ])

スプレッドでは、浅いフィールド のコピーと、明示的な割り当てと同じ変換を含む新しいレコードが作成されます。 フィールドによって参照される変更可能なオブジェクトは共有されたままです。

入れ子になったフィールドの更新は、コピーと更新の場合と同様に、スプレッドで機能します。

let original = {| Connection = local; Retries = 3 |}
let changed = {| ...original; Connection.Port = 5433 |}

assert (changed.Connection.Port = 5433)
assert (original.Connection.Port = 5432)

Connectionへの後の貢献により、そのフィールドに対する以前の入れ子になった更新が置き換えられます。 オーバーライドするスプレッドの後に意図的なオーバーライドを配置します。

シャドウされたフィールドに関する警告

後で拡散すると、明示的な選択が誤って元に戻される可能性があります。 コンパイラは、既定で次の警告を報告します。

Warning Meaning
FS3897 型のフィールドは、以前の明示的なフィールド宣言をシャドウします。
FS3898 式のフィールドは、以前の明示的なフィールド バインドをシャドウします。

たとえば、{ Host = "db.example.com"; ...local }は"db.example.com"ではなく、localからHostを受け取ります。

他の重複を監査するには、次の オプトイン警告を有効にします。

Warning Meaning
FS3905 タイプ スプレッドは、以前のタイプのスプレッドからフィールドをシャドウします。
FS3906 明示的なフィールドは、以前のスプレッドからフィールドをシャドウします。
FS3907 式の拡散は、前の式の拡散からフィールドをシャドウします。
<WarnOn>$(WarnOn);3905;3906;3907</WarnOn>

これらの通知では、右端の優先動作は変更されません。

表現と API の互換性

型は、コピー フィールドの変更可能性とフィールド レベルの属性を分散します。 型レベルの属性はコピーされません。変換先の独自の [<Struct>] 属性によって、その表現が決まります。 ソース型の引数は、測定単位の制約を含め、ソースのジェネリック制約を満たす必要があります。

Important

型スプレッドは、共有された進化が意図的な場合にのみ使用します。 ソース フィールドを変更すると、変換先のパブリック フィールド、コンストラクター、属性、または制約を、その変換先に対する編集なしで変更できます。

スプレッドはアクセシビリティ チェックをバイパスしません。 Null 許容レコードは有効な拡散ソースではありません。は、使用する前に null 許容値が null 以外であることを確立します。 スプレッドによる循環依存関係は拒否されます。 通常の再帰レコード フィールドは、宣言でスプレッドも使用されるため、禁止されていません。

type spreads work in .fsi files. 署名では明示的なフィールドを使用できますが、結果のフィールドとその順序が一致していれば、実装ではスプレッド (逆) が使用されます。 見積もりには、スプレッド ノードではなく、詳細なレコード構造が含まれています。

スプレッドまたはコピーアンドアップデートを選択する

1 つのソースからの同じ種類 の更新には、コピーと 更新の式を使用します。 スプレッドを使用して、ソースの結合、別のレコードの種類へのプロジェクト、またはフィールド宣言の再利用を行います。 同じレコード式で with と ... を組み合わせないでください。

スプレッドは、コレクション、オブジェクト式、任意の.NET オブジェクト、または C# レコードには適用されません。 レコード パターン構文は提供されません。 レコード コンストラクター は別の F# 11.2 機能です。

参照