Freigeben über


Richtlinien für das Formatieren von F#-Code

Dieser Artikel enthält Richtlinien zum Formatieren Ihres Codes. Damit machen Sie Ihren F#-Code:

  • Besser lesbar
  • Konform mit den Konventionen, die von Formatierungstools in Visual Studio Code und anderen Editoren angewandt werden
  • Ähnlich wie anderen Code im Internet

Weitere Informationen – auch zu Namenskonventionen – finden Sie unter Codierungskonventionen und Entwurfsrichtlinien für Komponenten.

Automatische Codeformatierung

Der Codeformatierer Fantomas ist das Standardtool der F#-Community für die automatische Codeformatierung. Die Standardeinstellungen entsprechen diesem Style Guide.

Es wird dringend empfohlen, diesen Codeformatierer zu verwenden. F#-Teams sollten sich auf einheitliche Regeln für die Codeformatierung einigen und diese in der Einstellungsdatei für den Codeformatierer festhalten, der dann in das Teamrepository eingecheckt wird.

Allgemeine Regeln für die Formatierung

F# ist sehr stark auf Leerraum ausgerichtet und deshalb in Bezug darauf sehr strikt. Die folgenden Richtlinien sollen als Orientierungshilfen für einige Herausforderungen dienen, die dies mit sich bringt.

Verwenden von Leerzeichen anstelle von Tabstoppzeichen

Wenn ein Einzug erforderlich ist, müssen Sie Leerzeichen anstelle von Tabstoppzeichen verwenden. In F#-Code werden keine Tabstoppzeichen verwendet, und der Compiler gibt einen Fehler aus, wenn außerhalb eines Zeichenfolgenliterals oder Kommentars ein Tabstoppzeichen gefunden wird.

Verwenden eines einheitlichen Einzugs

Für den Einzug ist mindestens ein Leerzeichen erforderlich. Ihre Organisation kann Codierungsstandards erstellen, um die Anzahl von Leerzeichen anzugeben, die für den Einzug verwendet werden sollen. In der Regel werden zwei, drei oder vier Leerzeichen als Einzug für jede Ebene verwendet.

Wir empfehlen vier Leerzeichen pro Einzug.

Der Einzug von Programmen ist jedoch eine subjektive Entscheidung. Abweichungen sind zwar möglich, aber die erste Regel, die Sie befolgen sollten, ist ein einheitlicher Einzug. Entscheiden Sie sich für einen allgemein akzeptierten Einzug, und wenden Sie ihn systematisch in Ihrer gesamten Codebasis an.

Vermeiden von Formatierungen mit Längenabhängigkeiten bei Namen

Versuchen Sie, Einzüge und Ausrichtungen zu vermeiden, die bei Namen Probleme verursachen könnten:

// ✔️ OK
let myLongValueName =
    someExpression
    |> anotherExpression

// ❌ Not OK
let myLongValueName = someExpression
                      |> anotherExpression

// ✔️ OK
let myOtherVeryLongValueName =
    match
        someVeryLongExpressionWithManyParameters
            parameter1
            parameter2
            parameter3
        with
    | Some _ -> ()
    | ...

// ❌ Not OK
let myOtherVeryLongValueName =
    match someVeryLongExpressionWithManyParameters parameter1
                                                   parameter2
                                                   parameter3 with
    | Some _ -> ()
    | ...

// ❌ Still Not OK
let myOtherVeryLongValueName =
    match someVeryLongExpressionWithManyParameters
              parameter1
              parameter2
              parameter3 with
    | Some _ -> ()
    | ...

Die Hauptgründe hierfür sind:

  • Wichtiger Code wird zu weit nach rechts verschoben.
  • Es bleibt weniger Breite für den tatsächlichen Code übrig.
  • Durch Umbenennen kann die Ausrichtung zerstört werden.

Vermeiden von überflüssigem Leerraum

Vermeiden Sie überflüssigen Leerraum in F#-Code, außer wenn er in diesem Style Guide beschrieben wird.

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

Formatieren von Kommentaren

Verwenden Sie lieber viele Kommentare mit doppelten Schrägstrichen als Kommentarblöcke.

// Prefer this style of comments when you want
// to express written ideas on multiple lines.

(*
    Block comments can be used, but use sparingly.
    They are useful when eliding code sections.
*)

In Kommentaren sollten der erste Buchstabe geschrieben werden. Außerdem sollten die Kommentare wohlformulierte Ausdrücke oder Sätze sein.

// ✔️ A good comment.
let f x = x + 1 // Increment by one.

// ❌ two poor comments
let f x = x + 1 // plus one

Informationen zum Formatieren von XML-Dokumentationskommentaren finden Sie weiter unten unter „Formatieren von Deklarationen“.

Formatieren von Ausdrücken

In diesem Abschnitt wird das Formatieren verschiedener Ausdrücke erläutert.

Formatieren von Zeichenfolgenausdrücken

Zeichenfolgenliterale und interpolierte Zeichenfolgen können einfach auf einer einzelnen Zeile stehen, unabhängig davon, wie lang die Zeile ist.

let serviceStorageConnection =
    $"DefaultEndpointsProtocol=https;AccountName=%s{serviceStorageAccount.Name};AccountKey=%s{serviceStorageAccountKey.Value}"

Von mehrzeiligen interpolierten Ausdrücken wird abgeraten. Binden Sie stattdessen das Ausdrucksergebnis an einen Wert, und verwenden Sie diesen in der interpolierten Zeichenfolge.

Formatieren von Tupelausdrücken

Eine Tupelinstanziierung sollte in Klammern stehen, und auf die als Trennzeichen verwendeten Kommas sollte ein einzelnes Leerzeichen folgen, z. B.: (1, 2), (x, y, z).

// ✔️ OK
let pair = (1, 2)
let triples = [ (1, 2, 3); (11, 12, 13) ]

Es ist allgemein akzeptiert, beim Musterabgleich von Tupeln die Klammern auszulassen:

// ✔️ OK
let (x, y) = z
let x, y = z

// ✔️ OK
match x, y with
| 1, _ -> 0
| x, 1 -> 0
| x, y -> 1

Üblicherweise werden die Klammern auch ausgelassen, wenn das Tupel der Rückgabewert einer Funktion ist:

// ✔️ OK
let update model msg =
    match msg with
    | 1 -> model + 1, []
    | _ -> model, [ msg ]

Zusammenfassend sollten Sie bei der Tupelinstanziierungen Klammern bevorzugen, aber bei der Verwendung von Tupeln für den Musterabgleich oder als Rückgabewert können Sie die Klammern auslassen.

Formatieren von Anwendungsausdrücken

Beim Formatieren einer Funktions- oder Methodenanwendung werden Argumente auf derselben Zeile bereitgestellt, sofern die Zeilenbreite dies zulässt:

// ✔️ OK
someFunction1 x.IngredientName x.Quantity

Verwenden Sie nur dann Klammern, wenn die Argumente dies erfordern:

// ✔️ OK
someFunction1 x.IngredientName

// ❌ Not preferred - parentheses should be omitted unless required
someFunction1 (x.IngredientName)

// ✔️ OK - parentheses are required
someFunction1 (convertVolumeToLiter x)

Verwenden Sie bei mehreren Argumente Leerzeichen:

// ✔️ OK
someFunction1 (convertVolumeToLiter x) (convertVolumeUSPint x)
someFunction2 (convertVolumeToLiter y) y
someFunction3 z (convertVolumeUSPint z)

// ❌ Not preferred - spaces should not be omitted between arguments
someFunction1(convertVolumeToLiter x)(convertVolumeUSPint x)
someFunction2(convertVolumeToLiter y) y
someFunction3 z(convertVolumeUSPint z)

Laut den Standardformatierungskonventionen sollte ein Leerzeichen eingefügt werden, wenn Funktionen in Kleinbuchstaben auf Argumente in Tupeln oder Klammern angewandt werden (selbst wenn nur ein einzelnes Argument verwendet wird):

// ✔️ OK
someFunction2 ()

// ✔️ OK
someFunction3 (x.Quantity1 + x.Quantity2)

// ❌ Not OK, formatting tools will add the extra space by default
someFunction2()

// ❌ Not OK, formatting tools will add the extra space by default
someFunction3(x.IngredientName, x.Quantity)

Gemäß den Standardformatkonventionen wird beim Anwenden groß geschriebener Methoden auf Argumente in Tupeln kein Leerraum hinzugefügt. Dies liegt daran, dass sie häufig bei der fluenten Programmierung verwendet werden:

// ✔️ OK - Methods accepting parenthesize arguments are applied without a space
SomeClass.Invoke()

// ✔️ OK - Methods accepting tuples are applied without a space
String.Format(x.IngredientName, x.Quantity)

// ❌ Not OK, formatting tools will remove the extra space by default
SomeClass.Invoke ()

// ❌ Not OK, formatting tools will remove the extra space by default
String.Format (x.IngredientName, x.Quantity)

Möglicherweise müssen Sie Argumente an eine Funktion auf einer neuen Zeile übergeben, damit sie besser lesbar sind oder weil Argumentlisten oder -namen zu lang sind. In diesem Fall wenden Sie eine Einzugebene an:

// ✔️ OK
someFunction2
    x.IngredientName x.Quantity

// ✔️ OK
someFunction3
    x.IngredientName1 x.Quantity2
    x.IngredientName2 x.Quantity2

// ✔️ OK
someFunction4
    x.IngredientName1
    x.Quantity2
    x.IngredientName2
    x.Quantity2

// ✔️ OK
someFunction5
    (convertVolumeToLiter x)
    (convertVolumeUSPint x)
    (convertVolumeImperialPint x)

Wenn die Funktion ein einzelnes mehrzeiliges Tuplerargument akzeptiert, platzieren Sie jedes Argument auf einer neuen Zeile:

// ✔️ OK
someTupledFunction (
    478815516,
    "A very long string making all of this multi-line",
    1515,
    false
)

// OK, but formatting tools will reformat to the above
someTupledFunction
    (478815516,
     "A very long string making all of this multi-line",
     1515,
     false)

Wenn Argumentausdrücke kurz sind, trennen Sie die Argumente durch Leerzeichen voneinander ab, und belassen Sie sie auf einer Zeile.

// ✔️ OK
let person = new Person(a1, a2)

// ✔️ OK
let myRegexMatch = Regex.Match(input, regex)

// ✔️ OK
let untypedRes = checker.ParseFile(file, source, opts)

Wenn Argumentausdrücke lang sind, verwenden Sie Einzelzeilen und eine Einzugebene, anstatt bis zur öffnenden Klammer einzurücken.

// ✔️ OK
let person =
    new Person(
        argument1,
        argument2
    )

// ✔️ OK
let myRegexMatch =
    Regex.Match(
        "my longer input string with some interesting content in it",
        "myRegexPattern"
    )

// ✔️ OK
let untypedRes =
    checker.ParseFile(
        fileName,
        sourceText,
        parsingOptionsWithDefines
    )

// ❌ Not OK, formatting tools will reformat to the above
let person =
    new Person(argument1,
               argument2)

// ❌ Not OK, formatting tools will reformat to the above
let untypedRes =
    checker.ParseFile(fileName,
                      sourceText,
                      parsingOptionsWithDefines)

Die gleichen Regeln gelten auch dann, wenn nur ein einzelnes mehrzeiliges Argument vorhanden ist, sowie bei mehrzeiligen Zeichenfolgen:

// ✔️ OK
let poemBuilder = StringBuilder()
poemBuilder.AppendLine(
    """
The last train is nearly due
The Underground is closing soon
And in the dark, deserted station
Restless in anticipation
A man waits in the shadows
    """
)

Option.traverse(
    create
    >> Result.setError [ invalidHeader "Content-Checksum" ]
)

Formatieren von Pipelineausdrücken

Wenn Sie Pipelineoperatoren |> auf mehreren Zeilen verwenden, sollten sie unter den Ausdrücken stehen, in denen sie verwendet werden.

// ✔️ OK
let methods2 =
    System.AppDomain.CurrentDomain.GetAssemblies()
    |> List.ofArray
    |> List.map (fun assm -> assm.GetTypes())
    |> Array.concat
    |> List.ofArray
    |> List.map (fun t -> t.GetMethods())
    |> Array.concat

// ❌ Not OK, add a line break after "=" and put multi-line pipelines on multiple lines.
let methods2 = System.AppDomain.CurrentDomain.GetAssemblies()
            |> List.ofArray
            |> List.map (fun assm -> assm.GetTypes())
            |> Array.concat
            |> List.ofArray
            |> List.map (fun t -> t.GetMethods())
            |> Array.concat

// ❌ Not OK either
let methods2 = System.AppDomain.CurrentDomain.GetAssemblies()
               |> List.ofArray
               |> List.map (fun assm -> assm.GetTypes())
               |> Array.concat
               |> List.ofArray
               |> List.map (fun t -> t.GetMethods())
               |> Array.concat

Formatieren von Lambdaausdrücken

Wenn ein Lambdaausdruck als Argument in einem mehrzeiligen Ausdruck und gefolgt von anderen Argumenten verwendet wird, platzieren Sie den Text des Lambdaausdrucks auf einer neuen Zeile, die um eine Ebene eingezogen ist:

// ✔️ OK
let printListWithOffset a list1 =
    List.iter
        (fun elem ->
             printfn $"A very long line to format the value: %d{a + elem}")
        list1

Wenn das Lambdaargument das letzte Argument in einer Funktionsanwendung ist, platzieren Sie alle Argumente bis zum Pfeil auf derselben Zeile.

// ✔️ OK
Target.create "Build" (fun ctx ->
    // code
    // here
    ())

// ✔️ OK
let printListWithOffsetPiped a list1 =
    list1
    |> List.map (fun x -> x + 1)
    |> List.iter (fun elem ->
        printfn $"A very long line to format the value: %d{a + elem}")

Auch Lambdaabgleiche sollten Sie auf ähnliche Weise behandeln.

// ✔️ OK
functionName arg1 arg2 arg3 (function
    | Choice1of2 x -> 1
    | Choice2of2 y -> 2)

Wenn vor dem Lambdaausdruck viele führende oder mehrzeilige Argumente stehen, sollten Sie alle Argumente um eine Ebene einziehen.

// ✔️ OK
functionName
    arg1
    arg2
    arg3
    (fun arg4 ->
        bodyExpr)

// ✔️ OK
functionName
    arg1
    arg2
    arg3
    (function
     | Choice1of2 x -> 1
     | Choice2of2 y -> 2)

Wenn der Text eines Lambdaausdrucks mehrere Zeilen lang ist, sollten Sie erwägen, ihn in eine lokale Funktion umzugestalten.

Wenn Pipelines Lambdaausdrücke enthalten, sind diese in der Regel das letzte Argument in jeder Phase der Pipeline:

// ✔️ OK, with 4 spaces indentation
let printListWithOffsetPiped list1 =
    list1
    |> List.map (fun elem -> elem + 1)
    |> List.iter (fun elem ->
        // one indent starting from the pipe
        printfn $"A very long line to format the value: %d{elem}")

// ✔️ OK, with 2 spaces indentation
let printListWithOffsetPiped list1 =
  list1
  |> List.map (fun elem -> elem + 1)
  |> List.iter (fun elem ->
    // one indent starting from the pipe
    printfn $"A very long line to format the value: %d{elem}")

Wenn die Argumente eines Lambdaausdrucks nicht auf eine Zeile passen oder selbst mehrzeilig sind, platzieren Sie sie auf der nächsten Zeile, und ziehen Sie diese um eine Ebene ein.

// ✔️ OK
fun
    (aVeryLongParameterName: AnEquallyLongTypeName)
    (anotherVeryLongParameterName: AnotherLongTypeName)
    (yetAnotherLongParameterName: LongTypeNameAsWell)
    (youGetTheIdeaByNow: WithLongTypeNameIncluded) ->
    // code starts here
    ()

// ❌ Not OK, code formatters will reformat to the above to respect the maximum line length.
fun (aVeryLongParameterName: AnEquallyLongTypeName) (anotherVeryLongParameterName: AnotherLongTypeName) (yetAnotherLongParameterName: LongTypeNameAsWell) (youGetTheIdeaByNow: WithLongTypeNameIncluded) ->
    ()

// ✔️ OK
let useAddEntry () =
    fun
        (input:
            {| name: string
               amount: Amount
               isIncome: bool
               created: string |}) ->
         // foo
         bar ()

// ❌ Not OK, code formatters will reformat to the above to avoid reliance on whitespace alignment that is contingent to length of an identifier.
let useAddEntry () =
    fun (input: {| name: string
                   amount: Amount
                   isIncome: bool
                   created: string |}) ->
        // foo
        bar ()

Formatieren von arithmetischen und binären Ausdrücken

Verwenden Sie immer Leerzeichen um binäre arithmetische Ausdrücke:

// ✔️ OK
let subtractThenAdd x = x - 1 + 3

Wenn der binäre --Operator in Kombination mit bestimmten Formatierungsoptionen nicht in Leerraum eingeschlossen ist, könnte er als der unäre --Operator interpretiert wird. Der unäre --Operatoren sollte immer unmittelbar auf den Wert folgen, den er negiert:

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

Das Hinzufügen eines Leerzeichens nach dem --Operator kann andere Personen verwirren.

Trennen Sie binäre Operatoren durch Leerzeichen voneinander ab. Infix-Ausdrücke können problemlos nach derselben Spalte ausgerichtet werden:

// ✔️ OK
let function1 () =
    acc +
    (someFunction
         x.IngredientName x.Quantity)

// ✔️ OK
let function1 arg1 arg2 arg3 arg4 =
    arg1 + arg2 +
    arg3 + arg4

Diese Regel gilt auch für Maßeinheiten in Anmerkungen zu Typen und Konstanten:

// ✔️ OK
type Test =
    { WorkHoursPerWeek: uint<hr / (staff weeks)> }
    static member create = { WorkHoursPerWeek = 40u<hr / (staff weeks)> }

// ❌ Not OK
type Test =
    { WorkHoursPerWeek: uint<hr/(staff weeks)> }
    static member create = { WorkHoursPerWeek = 40u<hr/(staff weeks)> }

Die folgenden Operatoren sind in der F#-Standardbibliothek definiert und sollten immer verwendet werden anstatt Entsprechungen zu definieren. Die Verwendung dieser Operatoren wird empfohlen, da sie den Code in der Regel besser lesbar und idiomatischer machen. In der folgenden Liste sind die empfohlenen F#-Operatoren zusammengefasst.

// ✔️ OK
x |> f // Forward pipeline
f >> g // Forward composition
x |> ignore // Discard away a value
x + y // Overloaded addition (including string concatenation)
x - y // Overloaded subtraction
x * y // Overloaded multiplication
x / y // Overloaded division
x % y // Overloaded modulus
x && y // Lazy/short-cut "and"
x || y // Lazy/short-cut "or"
x <<< y // Bitwise left shift
x >>> y // Bitwise right shift
x ||| y // Bitwise or, also for working with “flags” enumeration
x &&& y // Bitwise and, also for working with “flags” enumeration
x ^^^ y // Bitwise xor, also for working with “flags” enumeration

Formatieren von Bereichsoperatorausdrücke

Fügen Sie nur Leerzeichen um .. hinzu, wenn alle Ausdrücke nicht atomisch sind. Integer und Einzelwortbezeichner gelten als atomisch.

// ✔️ OK
let a = [ 2..7 ] // integers
let b = [ one..two ] // identifiers
let c = [ ..9 ] // also when there is only one expression
let d = [ 0.7 .. 9.2 ] // doubles
let e = [ 2L .. number / 2L ] // complex expression
let f = [| A.B .. C.D |] // identifiers with dots
let g = [ .. (39 - 3) ] // complex expression
let h = [| 1 .. MyModule.SomeConst |] // not all expressions are atomic

for x in 1..2 do
    printfn " x = %d" x

let s = seq { 0..10..100 }

// ❌ Not OK
let a = [ 2 .. 7 ]
let b = [ one .. two ]

Diese Regeln gelten auch für die Segmentierung:

// ✔️ OK
arr[0..10]
list[..^1]

Formatieren von if-Ausdrücken

Der Einzug von Bedingungen hängt von der Größe und Komplexität der Ausdrücke ab, aus denen sie bestehen. Schreiben Sie sie in folgenden Fällen auf einer Zeile:

  • cond, e1 und e2 sind kurz.
  • e1 und e2 sind selbst keine if/then/else-Ausdrücke.
// ✔️ OK
if cond then e1 else e2

Wenn es keinen else-Ausdruck gibt, wird empfohlen, den gesamten Ausdruck niemals auf einer Zeile zu schreiben. Damit soll imperativer Code besser von Funktionscode unterscheidbar bleiben.

// ✔️ OK
if a then
    ()

// ❌ Not OK, code formatters will reformat to the above by default
if a then ()

Wenn einer der Ausdrücke mehrzeilige ist, sollte jeder Teil der Bedingung mehrzeilig sein.

// ✔️ OK
if cond then
    let e1 = something()
    e1
else
    e2
    
// ❌ Not OK
if cond then
    let e1 = something()
    e1
else e2

Mehrere Bedingungen mit elif und else werden auf derselben Ebene wie if eingezogen, wenn sie den Regeln einzeiliger if/then/else-Ausdrücke entsprechen.

// ✔️ OK
if cond1 then e1
elif cond2 then e2
elif cond3 then e3
else e4

Wenn nur eine der Bedingungen oder Ausdrücke mehrzeilig ist, wird dadurch der gesamte if/then/else-Ausdruck mehrzeilig:

// ✔️ OK
if cond1 then
    let e1 = something()
    e1
elif cond2 then
    e2
elif cond3 then
    e3
else
    e4

// ❌ Not OK
if cond1 then
    let e1 = something()
    e1
elif cond2 then e2
elif cond3 then e3
else e4

Wenn eine Bedingung mehrzeilig ist oder die Standardtoleranz für einzeilige Bedingungen überschreitet, sollte der Bedingungsausdruck mit Einzug auf einer neuen Zeile stehen. Die Schlüsselwörter if und then sollten beim Kapseln des Ausdrucks einer langen Bedingung gleich ausgerichtet sein.

// ✔️ OK, but better to refactor, see below
if
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree 
then
        e1
    else
        e2

// ✔️The same applies to nested `elif` or `else if` expressions
if a then
    b
elif
    someLongFunctionCall
        argumentOne
        argumentTwo
        argumentThree
        argumentFour
then
    c
else if
    someOtherLongFunctionCall
        argumentOne
        argumentTwo
        argumentThree
        argumentFour
then
    d

Es gilt jedoch als besserer Stil, lange Bedingungen in eine let-Bindung oder separate Funktion umzugestalten:

// ✔️ OK
let performAction =
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree

if performAction then
    e1
else
    e2

Formatieren von Union-Fall-Ausdrücken

Das Anwenden unterscheidbarer Union-Fälle folgt den gleichen Regeln wie Funktions- und Methodenanwendungen. Das heißt, da der Name groß geschrieben ist, entfernen Codeformatierer das Leerzeichen vor einem Tupel:

// ✔️ OK
let opt = Some("A", 1)

// OK, but code formatters will remove the space
let opt = Some ("A", 1)

Wie bei Funktionsanwendungen sollten mehrzeilige Konstruktionen eingezogen werden:

// ✔️ OK
let tree1 =
    BinaryNode(
        BinaryNode (BinaryValue 1, BinaryValue 2),
        BinaryNode (BinaryValue 3, BinaryValue 4)
    )

Formatieren von Listen- und Arrayausdrücken

Sie schreiben x :: l mit Leerzeichen um den ::-Operator (:: ist ein Infix-Operator und steht daher zwischen Leerzeichen).

Bei Listen und Arrays, die auf einer einzelnen Zeile deklariert werden, sollte nach der öffnenden Klammer und vor der schließenden Klammer ein Leerzeichen stehen:

// ✔️ OK
let xs = [ 1; 2; 3 ]

// ✔️ OK
let ys = [| 1; 2; 3; |]

Verwenden Sie immer mindestens ein Leerzeichen zwischen Operatoren mit unterschiedlichen Klammern. Setzen Sie z. B. ein Leerzeichen zwischen [ und {.

// ✔️ OK
[ { Ingredient = "Green beans"; Quantity = 250 }
  { Ingredient = "Pine nuts"; Quantity = 250 }
  { Ingredient = "Feta cheese"; Quantity = 250 }
  { Ingredient = "Olive oil"; Quantity = 10 }
  { Ingredient = "Lemon"; Quantity = 1 } ]

// ❌ Not OK
[{ Ingredient = "Green beans"; Quantity = 250 }
 { Ingredient = "Pine nuts"; Quantity = 250 }
 { Ingredient = "Feta cheese"; Quantity = 250 }
 { Ingredient = "Olive oil"; Quantity = 10 }
 { Ingredient = "Lemon"; Quantity = 1 }]

Dasselbe gilt auch für Listen oder Arrays mit Tupeln.

Für mehrzeilige Listen und Arrays gilt eine ähnliche Regel wie für Datensätze:

// ✔️ OK
let pascalsTriangle =
    [| [| 1 |]
       [| 1; 1 |]
       [| 1; 2; 1 |]
       [| 1; 3; 3; 1 |]
       [| 1; 4; 6; 4; 1 |]
       [| 1; 5; 10; 10; 5; 1 |]
       [| 1; 6; 15; 20; 15; 6; 1 |]
       [| 1; 7; 21; 35; 35; 21; 7; 1 |]
       [| 1; 8; 28; 56; 70; 56; 28; 8; 1 |] |]

Wie bei Datensätzen vereinfacht das Deklarieren der öffnenden und schließenden Klammern auf einer eigenen Zeile das Verschieben von Code und das Einfügen in Funktionen:

// ✔️ OK
let pascalsTriangle =
    [| 
        [| 1 |]
        [| 1; 1 |]
        [| 1; 2; 1 |]
        [| 1; 3; 3; 1 |]
        [| 1; 4; 6; 4; 1 |]
        [| 1; 5; 10; 10; 5; 1 |]
        [| 1; 6; 15; 20; 15; 6; 1 |]
        [| 1; 7; 21; 35; 35; 21; 7; 1 |]
        [| 1; 8; 28; 56; 70; 56; 28; 8; 1 |] 
    |]

Wenn ein Listen- oder Arrayausdruck die rechte Seite einer Bindung ist, können Sie auch den Stroustrup-Stil verwenden:

// ✔️ OK
let pascalsTriangle = [| 
   [| 1 |]
   [| 1; 1 |]
   [| 1; 2; 1 |]
   [| 1; 3; 3; 1 |]
   [| 1; 4; 6; 4; 1 |]
   [| 1; 5; 10; 10; 5; 1 |]
   [| 1; 6; 15; 20; 15; 6; 1 |]
   [| 1; 7; 21; 35; 35; 21; 7; 1 |]
   [| 1; 8; 28; 56; 70; 56; 28; 8; 1 |] 
|]

Bei Listen- oder Arrayausdrücken, die nicht die rechte Seite einer Bindung sind, sich z. B. innerhalb einer anderen Liste oder eines anderen Arrays befinden und sich über mehrere Zeilen erstrecken, sollten die Klammern auf eigenen Zeilen stehen:

// ✔️ OK - The outer list follows `Stroustrup` style, while the inner lists place their brackets on separate lines
let fn a b = [ 
    [
        someReallyLongValueThatWouldForceThisListToSpanMultipleLines
        a
    ]
    [ 
        b
        someReallyLongValueThatWouldForceThisListToSpanMultipleLines 
    ]
]

// ❌ Not okay
let fn a b = [ [
    someReallyLongValueThatWouldForceThisListToSpanMultipleLines
    a
]; [
    b
    someReallyLongValueThatWouldForceThisListToSpanMultipleLines
] ]

Dieselbe Regel gilt auch für Datensatztypen innerhalb von Arrays oder Listen:

// ✔️ OK - The outer list follows `Stroustrup` style, while the inner lists place their brackets on separate lines
let fn a b = [ 
    {
        Foo = someReallyLongValueThatWouldForceThisListToSpanMultipleLines
        Bar = a
    }
    { 
        Foo = b
        Bar = someReallyLongValueThatWouldForceThisListToSpanMultipleLines 
    }
]

// ❌ Not okay
let fn a b = [ {
    Foo = someReallyLongValueThatWouldForceThisListToSpanMultipleLines
    Bar = a
}; {
    Foo = b
    Bar = someReallyLongValueThatWouldForceThisListToSpanMultipleLines
} ]

Wenn Sie Arrays und Listen programmgesteuert generieren, sollten Sie -> gegenüber do ... yield vorziehen, wenn immer ein Wert generiert wird:

// ✔️ OK
let squares = [ for x in 1..10 -> x * x ]

// ❌ Not preferred, use "->" when a value is always generated
let squares' = [ for x in 1..10 do yield x * x ]

In älteren Versionen von F# musste in Situationen, in denen Daten bedingt generiert wurden oder aufeinanderfolgende Ausdrücke ausgewertet werden mussten, yield angegeben werden. Lassen Sie das Schlüsselwort yield lieber weg, sofern Sie nicht mit einer älteren F#-Version kompilieren:

// ✔️ OK
let daysOfWeek includeWeekend =
    [
        "Monday"
        "Tuesday"
        "Wednesday"
        "Thursday"
        "Friday"
        if includeWeekend then
            "Saturday"
            "Sunday"
    ]

// ❌ Not preferred - omit yield instead
let daysOfWeek' includeWeekend =
    [
        yield "Monday"
        yield "Tuesday"
        yield "Wednesday"
        yield "Thursday"
        yield "Friday"
        if includeWeekend then
            yield "Saturday"
            yield "Sunday"
    ]

In einigen Fällen kann do...yield jedoch die Lesbarkeit verbessern. Diese Sonderfälle sollten berücksichtigt werden.

Formatieren von Datensatzausdrücken

Kurze Datensätze können auf einer Zeile geschrieben werden:

// ✔️ OK
let point = { X = 1.0; Y = 0.0 }

Bei längeren Datensätzen sollten für Bezeichnungen neue Zeilen verwendet werden:

// ✔️ OK
let rainbow =
    { Boss = "Jeffrey"
      Lackeys = ["Zippy"; "George"; "Bungle"] }

Formatieren von mehrzeiligen Klammerausdrücken

Für Datensätze, die sich über mehrere Zeilen erstrecken, gibt es drei häufig verwendete Formatierungsstile: Cramped, Aligned und Stroustrup. Der Cramped-Stil ist der Standardstil für F#-Code, da er tendenziell Stile fördert, die das Parsen von Code durch den Compiler vereinfachen. Die Stile Aligned und Stroustrup vereinfachen die Neuanordnung von Membern, sodass der Code einfacher umgestaltet werden kann. Sie haben allerdings den Nachteil, dass in bestimmten Situationen möglicherweise etwas ausführlicherer Code erforderlich ist.

  • Cramped: Der traditionelle Standard und das Standardformat für F#-Datensätze. Öffnende Klammern stehen auf derselben Zeile wie das erste Element und schließende Klammern auf derselben Zeile wie das letzte Element.

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned: Klammern stehen auf einer eigenen Zeile mit identischem Einzug.

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup: Die öffnende Klammer steht auf derselben Zeile wie die Bindung und die schließende Klammer auf einer eigenen Zeile.

    let rainbow = {
        Boss1 = "Jeffrey"
        Boss2 = "Jeffrey"
        Boss3 = "Jeffrey"
        Lackeys = [ "Zippy"; "George"; "Bungle" ]
    }
    

Diese Formatierungsstilregeln gelten auch für Listen- und Arrayelemente.

Formatieren von Copy-and-Update-Record-Ausdrücken

Ein Copy-and-Update-Ausdruck verarbeitet immer noch Datensätze, sodass ähnliche Richtlinien gelten.

Kurze Ausdrücke können auf einer Zeile stehen:

// ✔️ OK
let point2 = { point with X = 1; Y = 2 }

Für längere Ausdrücke sollten mehrere Zeilen verwendet werden, und sie sollten basierend auf einer der oben genannten Konventionen formatiert werden:

// ✔️ OK - Cramped
let newState =
    { state with
        Foo =
            Some
                { F1 = 0
                  F2 = "" } }
        
// ✔️ OK - Aligned
let newState = 
    {
        state with
            Foo =
                Some
                    {
                        F1 = 0
                        F2 = ""
                    }
    }

// ✔️ OK - Stroustrup
let newState = { 
    state with
        Foo =
            Some { 
                F1 = 0
                F2 = ""
            }
}

Hinweis: Wenn Sie den Stroustrup-Stil für Copy-and-Update-Ausdrücke verwenden, müssen Sie die Member weiter einziehen als den kopierten Datensatznamen:

// ✔️ OK
let bilbo = {
    hobbit with 
        Name = "Bilbo"
        Age = 111
        Region = "The Shire" 
}

// ❌ Not OK - Results in compiler error: "Possible incorrect indentation: this token is offside of context started at position"
let bilbo = {
    hobbit with 
    Name = "Bilbo"
    Age = 111
    Region = "The Shire" 
}

Formatieren von Musterabgleichen

Verwenden Sie für jede Klausel eines Abgleichs | ohne Einzug. Wenn der Ausdruck kurz ist, können Sie ihn auch auf einer einzelnen Zeile schreiben, sofern die Teilausdrücke ebenfalls einfach sind.

// ✔️ OK
match l with
| { him = x; her = "Posh" } :: tail -> x
| _ :: tail -> findDavid tail
| [] -> failwith "Couldn't find David"

// ❌ Not OK, code formatters will reformat to the above by default
match l with
    | { him = x; her = "Posh" } :: tail -> x
    | _ :: tail -> findDavid tail
    | [] -> failwith "Couldn't find David"

Wenn der Ausdruck auf der rechten Seite des Musterabgleichspfeils zu groß ist, verschieben Sie ihn auf die nächste Zeile und ziehen ihn um eine Ebene gegenüber match/| ein.

// ✔️ OK
match lam with
| Var v -> 1
| Abs(x, body) ->
    1 + sizeLambda body
| App(lam1, lam2) ->
    sizeLambda lam1 + sizeLambda lam2

Ähnlich wie bei großen if-Bedingungen sollten match-Ausdrücke, die mehrzeilig sind oder die Standardtoleranz für einzeilige Ausdrücke überschreiten, auf eine neue Zeile verschoben und eingezogen werden. Die Schlüsselwörter match und with sollten beim Kapseln des langen match-Ausdrucks gleich ausgerichtet sein.

// ✔️ OK, but better to refactor, see below
match
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree 
with
| X y -> y
| _ -> 0

Es gilt jedoch als besserer Stil, lange match-Ausdrücke in eine let-Bindung oder separate Funktion umzugestalten:

// ✔️ OK
let performAction =
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree

match performAction with
| X y -> y
| _ -> 0

Das Ausrichten der Pfeile eines Musterabgleichs sollte vermieden werden.

// ✔️ OK
match lam with
| Var v -> v.Length
| Abstraction _ -> 2

// ❌ Not OK, code formatters will reformat to the above by default
match lam with
| Var v         -> v.Length
| Abstraction _ -> 2

Ein Musterabgleich, der mithilfe des Schlüsselworts function eingeführt wurde, sollte eine Ebene vom Anfang der vorherigen Zeile eingezogen werden:

// ✔️ OK
lambdaList
|> List.map (function
    | Abs(x, body) -> 1 + sizeLambda 0 body
    | App(lam1, lam2) -> sizeLambda (sizeLambda 0 lam1) lam2
    | Var v -> 1)

In Funktionen, die mit let oder let rec definiert werden, sollte im Allgemeinen match anstelle von function verwendet werden. Bei Verwendung von Musterregeln sollten diese am Schlüsselwort function ausgerichtet werden:

// ✔️ OK
let rec sizeLambda acc =
    function
    | Abs(x, body) -> sizeLambda (succ acc) body
    | App(lam1, lam2) -> sizeLambda (sizeLambda acc lam1) lam2
    | Var v -> succ acc

Formatieren von try/with-Ausdrücken

Ein Musterabgleich mit Ausnahmetypen sollte auf derselben Ebene wie with eingezogen werden.

// ✔️ OK
try
    if System.DateTime.Now.Second % 3 = 0 then
        raise (new System.Exception())
    else
        raise (new System.ApplicationException())
with
| :? System.ApplicationException ->
    printfn "A second that was not a multiple of 3"
| _ ->
    printfn "A second that was a multiple of 3"

Bei mehreren Klauseln fügen Sie für jede Klausel ein | hinzu:

// ✔️ OK
try
    persistState currentState
with ex ->
    printfn "Something went wrong: %A" ex

// ✔️ OK
try
    persistState currentState
with :? System.ApplicationException as ex ->
    printfn "Something went wrong: %A" ex

// ❌ Not OK, see above for preferred formatting
try
    persistState currentState
with
| ex ->
    printfn "Something went wrong: %A" ex

// ❌ Not OK, see above for preferred formatting
try
    persistState currentState
with
| :? System.ApplicationException as ex ->
    printfn "Something went wrong: %A" ex

Formatieren benannter Argumente

Bei benannten Argumenten sollte das = von Leerzeichen eingeschlossen sein:

// ✔️ OK
let makeStreamReader x = new System.IO.StreamReader(path = x)

// ❌ Not OK, spaces are necessary around '=' for named arguments
let makeStreamReader x = new System.IO.StreamReader(path=x)

Bei einem Musterabgleich mit unterscheidbaren Unions werden benannte Muster ähnlich formatiert, z. B.:

type Data =
    | TwoParts of part1: string * part2: string
    | OnePart of part1: string

// ✔️ OK
let examineData x =
    match data with
    | OnePartData(part1 = p1) -> p1
    | TwoPartData(part1 = p1; part2 = p2) -> p1 + p2

// ❌ Not OK, spaces are necessary around '=' for named pattern access
let examineData x =
    match data with
    | OnePartData(part1=p1) -> p1
    | TwoPartData(part1=p1; part2=p2) -> p1 + p2

Formatieren von Mutationsausdrücken

Mutationsausdrücke der Form location <- expr werden normalerweise auf einer Zeile geschrieben. Wenn mehrere Zeilen erforderlich sind, platzieren Sie den Ausdruck auf der Seiten auf einer neuen Zeile.

// ✔️ OK
ctx.Response.Headers[HeaderNames.ContentType] <-
    Constants.jsonApiMediaType |> StringValues

ctx.Response.Headers[HeaderNames.ContentLength] <-
    bytes.Length |> string |> StringValues

// ❌ Not OK, code formatters will reformat to the above by default
ctx.Response.Headers[HeaderNames.ContentType] <- Constants.jsonApiMediaType
                                                 |> StringValues
ctx.Response.Headers[HeaderNames.ContentLength] <- bytes.Length
                                                   |> string
                                                   |> StringValues

Formatieren von Objektausdrücken

Member von Objektausdrücken sollten ausgerichtet an member um eine Ebene eingezogen werden.

// ✔️ OK
let comparer =
    { new IComparer<string> with
          member x.Compare(s1, s2) =
              let rev (s: String) = new String (Array.rev (s.ToCharArray()))
              let reversed = rev s1
              reversed.CompareTo (rev s2) }

Sie können auf den Stroustrup-Stil in Erwägung ziehen:

let comparer = { 
    new IComparer<string> with
        member x.Compare(s1, s2) =
            let rev (s: String) = new String(Array.rev (s.ToCharArray()))
            let reversed = rev s1
            reversed.CompareTo(rev s2)
}

Leere Typdefinitionen können in einer Zeile formatiert werden:

type AnEmptyType = class end

Unabhängig von der gewählten Seitenbreite sollte = class end immer in der gleichen Zeile stehen.

Formatieren von Index-/Segmentausdrücken

Indexausdrücke dürfen keine Leerzeichen um die öffnenden und schließenden Klammern enthalten.

// ✔️ OK
let v = expr[idx]
let y = myList[0..1]

// ❌ Not OK
let v = expr[ idx ]
let y = myList[ 0 .. 1 ]

Dies gilt auch für die ältere expr.[idx]-Syntax.

// ✔️ OK
let v = expr.[idx]
let y = myList.[0..1]

// ❌ Not OK
let v = expr.[ idx ]
let y = myList.[ 0 .. 1 ]

Formatieren von Ausdrücken in Anführungszeichen

Die Trennzeichensymbole (<@, @>, <@@, @@>) sollten in separaten Zeilen platziert werden, wenn der anführungszeichenausdruck ein mehrzeiliger Ausdruck ist.

// ✔️ OK
<@
    let f x = x + 10
    f 20
@>

// ❌ Not OK
<@ let f x = x + 10
   f 20
@>

Bei einzeiligen Ausdrücken sollten die Trennzeichensymbole auf derselben Zeile wie der Ausdruck selbst stehen.

// ✔️ OK
<@ 1 + 1 @>

// ❌ Not OK
<@
    1 + 1
@>

Formatieren verketteter Ausdrücke

Wenn verkettete Ausdrücke (mit . verknüpfte Funktionsanwendungen) lang sind, schreiben Sie jeden Anwendungsaufruf auf eine eigene Zeile. Ziehen Sie die aufeinanderfolgenden Glieder in der Kette um jeweils eine Ebene ein.

// ✔️ OK
Host
    .CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(fun webBuilder -> webBuilder.UseStartup<Startup>())

// ✔️ OK
Cli
    .Wrap("git")
    .WithArguments(arguments)
    .WithWorkingDirectory(__SOURCE_DIRECTORY__)
    .ExecuteBufferedAsync()
    .Task

Das erste Glied kann aus mehreren Gliedern bestehen, wenn es sich um einfache Bezeichner handelt. Ein Beispiel ist das Hinzufügen eines vollqualifizierten Namespace.

// ✔️ OK
Microsoft.Extensions.Hosting.Host
    .CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(fun webBuilder -> webBuilder.UseStartup<Startup>())

Die nachfolgenden Glieder sollten ebenfalls einfache Bezeichner enthalten.

// ✔️ OK
configuration.MinimumLevel
    .Debug()
    // Notice how `.WriteTo` does not need its own line.
    .WriteTo.Logger(fun loggerConfiguration ->
        loggerConfiguration.Enrich
            .WithProperty("host", Environment.MachineName)
            .Enrich.WithProperty("user", Environment.UserName)
            .Enrich.WithProperty("application", context.HostingEnvironment.ApplicationName))

Wenn die Argumente in einer Funktionsanwendung nicht auf eine Zeile passen, platzieren Sie jedes Argument auf einer eigenen Zeile.

// ✔️ OK
WebHostBuilder()
    .UseKestrel()
    .UseUrls("http://*:5000/")
    .UseCustomCode(
        longArgumentOne,
        longArgumentTwo,
        longArgumentThree,
        longArgumentFour
    )
    .UseContentRoot(Directory.GetCurrentDirectory())
    .UseStartup<Startup>()
    .Build()

// ✔️ OK
Cache.providedTypes
    .GetOrAdd(cacheKey, addCache)
    .Value

// ❌ Not OK, formatting tools will reformat to the above
Cache
    .providedTypes
    .GetOrAdd(
        cacheKey,
        addCache
    )
    .Value

Lambdaargumente innerhalb einer Funktionsanwendung sollten auf derselben Zeile wie die öffnende ( beginnen.

// ✔️ OK
builder
    .WithEnvironment()
    .WithLogger(fun loggerConfiguration ->
        // ...
        ())

// ❌ Not OK, formatting tools will reformat to the above
builder
    .WithEnvironment()
    .WithLogger(
        fun loggerConfiguration ->
        // ...
        ())

Formatieren von Deklarationen

In diesem Abschnitt wird das Formatieren verschiedener Deklarationen erläutert.

Einfügen von Leerzeilen zwischen Deklarationen

Trennen Sie Funktions- und Klassendefinitionen der obersten Ebene durch eine Leerzeile. Beispiel:

// ✔️ OK
let thing1 = 1+1

let thing2 = 1+2

let thing3 = 1+3

type ThisThat = This | That

// ❌ Not OK
let thing1 = 1+1
let thing2 = 1+2
let thing3 = 1+3
type ThisThat = This | That

Wenn ein Konstrukt XML-Dokumentationskommentare enthält, fügen Sie vor dem Kommentar eine Leerzeile ein.

// ✔️ OK

/// This is a function
let thisFunction() =
    1 + 1

/// This is another function, note the blank line before this line
let thisFunction() =
    1 + 1

Formatieren von let- und member-Deklarationen

Bei let- und member-Deklarationen wird die rechte Seite einer Bindung in der Regel entweder auf einer Zeile oder (wenn sie zu lang ist) auf einer neuen Zeile um eine Ebene eingezogen platziert.

Die folgenden Beispiele sind beispielsweise konform:

// ✔️ OK
let a =
    """
foobar, long string
"""

// ✔️ OK
type File =
    member this.SaveAsync(path: string) : Async<unit> =
        async {
            // IO operation
            return ()
        }

// ✔️ OK
let c =
    { Name = "Bilbo"
      Age = 111
      Region = "The Shire" }

// ✔️ OK
let d =
    while f do
        printfn "%A" x

Diese Beispiele sind nicht konform:

// ❌ Not OK, code formatters will reformat to the above by default
let a = """
foobar, long string
"""

let d = while f do
    printfn "%A" x

Bei Instanziierungen von Datensatztypen können die Klammern auch auf eigenen Zeilen platziert werden:

// ✔️ OK
let bilbo =
    { 
        Name = "Bilbo"
        Age = 111
        Region = "The Shire" 
    }

Sie können auch den Stroustrup-Stil in Erwägung ziehen, bei dem die öffnende { auf derselben Zeile wie der Bindungsname steht:

// ✔️ OK
let bilbo = {
    Name = "Bilbo"
    Age = 111
    Region = "The Shire"
}

Trennen Sie Member durch eine Leerzeile voneinander ab, und fügen Sie einen Dokumentationskommentar hinzu:

// ✔️ OK

/// This is a thing
type ThisThing(value: int) =

    /// Gets the value
    member _.Value = value

    /// Returns twice the value
    member _.TwiceValue() = value*2

Weitere Leerzeilen können (sparsam) verwendet werden, um Gruppen verwandter Funktionen zu trennen. Leerzeilen zwischen verwandten einzeiligen Ausdrücken können ausgelassen werden (z. B. bei mehreren Platzhalterimplementierungen). Verwenden Sie Leerzeilen in Funktionen (umsichtig), um logische Abschnitte zu verdeutlichen.

Formatieren von Funktions- und Memberargumenten

Wenn Sie eine Funktion definieren, schließen Sie jedes Argument in Leerzeichen ein.

// ✔️ OK
let myFun (a: decimal) (b: int) c = a + b + c

// ❌ Not OK, code formatters will reformat to the above by default
let myFunBad (a:decimal)(b:int)c = a + b + c

Bei einer langen Funktionsdefinition platzieren Sie die Parameter auf neuen Zeilen und ziehen sie ein. Die Einzugsebene muss der der nachfolgenden Parameter entsprechen.

// ✔️ OK
module M =
    let longFunctionWithLotsOfParameters
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        =
        // ... the body of the method follows

    let longFunctionWithLotsOfParametersAndReturnType
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        : ReturnType =
        // ... the body of the method follows

    let longFunctionWithLongTupleParameter
        (
            aVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse
        ) =
        // ... the body of the method follows

    let longFunctionWithLongTupleParameterAndReturnType
        (
            aVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse
        ) : ReturnType =
        // ... the body of the method follows

Dies gilt auch für Member, Konstruktoren und Parameter, die Tupel verwenden:

// ✔️ OK
type TypeWithLongMethod() =
    member _.LongMethodWithLotsOfParameters
        (
            aVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse
        ) =
        // ... the body of the method

// ✔️ OK
type TypeWithLongConstructor
    (
        aVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
        aSecondVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
        aThirdVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse
    ) =
    // ... the body of the class follows

// ✔️ OK
type TypeWithLongSecondaryConstructor () =
    new
        (
            aVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse
        ) =
        // ... the body of the constructor follows

Wenn die Parameter zusammengesetzt sind, platzieren Sie das =-Zeichen zusammen mit etwaigen Rückgabetypen auf einer neuen Zeile:

// ✔️ OK
type TypeWithLongCurriedMethods() =
    member _.LongMethodWithLotsOfCurriedParamsAndReturnType
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        : ReturnType =
        // ... the body of the method

    member _.LongMethodWithLotsOfCurriedParams
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        =
        // ... the body of the method

Auf diese Weise können Sie lange Zeilen (falls der Rückgabetyp einen langen Namen haben kann) und Zeilenfehler beim Hinzufügen von Parametern vermeiden.

Formatieren von Operatordeklarationen

Verwenden Sie optional Leerzeichen um Operatordefinitionen:

// ✔️ OK
let ( !> ) x f = f x

// ✔️ OK
let (!>) x f = f x

Bei benutzerdefinierten Operatoren, die mit * beginnen und mehrere Zeichen enthalten, müssen Sie am Anfang der Definition Leerraum einfügen, um Mehrdeutigkeiten beim Compiler zu vermeiden. Daher wird empfohlen, einfach die Definitionen aller Operatoren in einzelne Leerzeichen einzuschließen.

Formatieren von Datensatzdeklarationen

Bei Datensatzdeklarationen sollten Sie standardmäßig die { in der Typdefinition um vier Leerzeichen einziehen, die Bezeichnungsliste auf derselben Zeile beginnen und ggf. Member an der { ausrichten:

// ✔️ OK
type PostalAddress =
    { Address: string
      City: string
      Zip: string }

Es ist auch üblich, Klammern auf einer eigenen Zeile zu schreiben. In diesem Fall werden die Bezeichnungen um zusätzliche vier Leerzeichen eingezogen:

// ✔️ OK
type PostalAddress =
    { 
        Address: string
        City: string
        Zip: string 
    }

Sie können die { auch am Ende der ersten Zeile der Typdefinition (Stroustrup-Stil) platzieren:

// ✔️ OK
type PostalAddress = {
    Address: string
    City: string
    Zip: string
}

Wenn zusätzliche Member benötigt werden, verwenden Sie möglichst nicht with/end:

// ✔️ OK
type PostalAddress =
    { Address: string
      City: string
      Zip: string }
    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ❌ Not OK, code formatters will reformat to the above by default
type PostalAddress =
    { Address: string
      City: string
      Zip: string }
  with
    member x.ZipAndCity = $"{x.Zip} {x.City}"
  end
  
// ✔️ OK
type PostalAddress =
    { 
        Address: string
        City: string
        Zip: string 
    }
    member x.ZipAndCity = $"{x.Zip} {x.City}"
    
// ❌ Not OK, code formatters will reformat to the above by default
type PostalAddress =
    { 
        Address: string
        City: string
        Zip: string 
    }
    with
        member x.ZipAndCity = $"{x.Zip} {x.City}"
    end

Sie weichen von dieser Formatierungsregel ab, wenn Sie Datensätze im Stroustrup-Stil formatieren. In diesem Fall ist aufgrund von Compilerregeln das Schlüsselwort with erforderlich, wenn Sie eine Schnittstelle implementieren oder zusätzliche Member hinzufügen möchten:

// ✔️ OK
type PostalAddress = {
    Address: string
    City: string
    Zip: string
} with
    member x.ZipAndCity = $"{x.Zip} {x.City}"
   
// ❌ Not OK, this is currently invalid F# code
type PostalAddress = {
    Address: string
    City: string
    Zip: string
} 
member x.ZipAndCity = $"{x.Zip} {x.City}"

Beim Hinzufügen von XML-Dokumentation für Datensatzfelder wird der Aligned- oder Stroustrup-Stil bevorzugt. Außerdem sollten Sie zusätzliche Leerzeichen zwischen den Membern einfügen:

// ❌ Not OK - putting { and comments on the same line should be avoided
type PostalAddress =
    { /// The address
      Address: string

      /// The city
      City: string

      /// The zip code
      Zip: string }

    /// Format the zip code and the city
    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ✔️ OK
type PostalAddress =
    {
        /// The address
        Address: string

        /// The city
        City: string

        /// The zip code
        Zip: string
    }

    /// Format the zip code and the city
    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ✔️ OK - Stroustrup Style
type PostalAddress = {
    /// The address
    Address: string

    /// The city
    City: string

    /// The zip code
    Zip: string
} with
    /// Format the zip code and the city
    member x.ZipAndCity = $"{x.Zip} {x.City}"

Das öffnende Token auf eine und das schließende Token auf eine neue Zeile zu platzieren ist vorzuziehen, wenn Sie Schnittstellenimplementierungen oder Member im Datensatz deklarieren:

// ✔️ OK
// Declaring additional members on PostalAddress
type PostalAddress =
    {
        /// The address
        Address: string

        /// The city
        City: string

        /// The zip code
        Zip: string
    }

    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ✔️ OK
type MyRecord =
    {
        /// The record field
        SomeField: int
    }
    interface IMyInterface

Diese Regeln gelten auch für anonyme Datensatztypaliase.

Formatieren unterscheidbarer Union-Deklarationen

Bei unterscheidbaren Union-Deklarationen ziehen Sie den | in der Typdefinition um vier Leerzeichen ein:

// ✔️ OK
type Volume =
    | Liter of float
    | FluidOunce of float
    | ImperialPint of float

// ❌ Not OK
type Volume =
| Liter of float
| USPint of float
| ImperialPint of float

Bei nur einer kurzen Union können Sie das führende |-Zeichen auslassen.

// ✔️ OK
type Address = Address of string

Behalten Sie bei einer längeren oder mehrzeiligen Union das | bei, und platzieren Sie jedes Feld der Union auf einer neuen Zeile mit dem trennenden * am Zeilenende.

// ✔️ OK
[<NoEquality; NoComparison>]
type SynBinding =
    | SynBinding of
        accessibility: SynAccess option *
        kind: SynBindingKind *
        mustInline: bool *
        isMutable: bool *
        attributes: SynAttributes *
        xmlDoc: PreXmlDoc *
        valData: SynValData *
        headPat: SynPat *
        returnInfo: SynBindingReturnInfo option *
        expr: SynExpr *
        range: range *
        seqPoint: DebugPointAtBinding

Für Dokumentationskommentare verwenden Sie für jeden ///-Kommentar eine eigene Zeile.

// ✔️ OK

/// The volume
type Volume =

    /// The volume in liters
    | Liter of float

    /// The volume in fluid ounces
    | FluidOunce of float

    /// The volume in imperial pints
    | ImperialPint of float

Formatieren von Literaldeklarationen

Bei F#-Literalen mit einem Literal-Attribut sollte dieses auf einer eigenen Zeile stehen und mit PascalCase-Notation benannt werden:

// ✔️ OK

[<Literal>]
let Path = __SOURCE_DIRECTORY__ + "/" + __SOURCE_FILE__

[<Literal>]
let MyUrl = "www.mywebsitethatiamworkingwith.com"

Keinesfalls sollten Sie das Attribut auf derselben Zeile wie den Wert platzieren.

Formatieren von Moduldeklarationen

Code in einem lokalen Modul muss relativ zum Modul eingezogen werden, der Code in einem Modul der obersten Ebene sollte jedoch nicht eingezogen werden. Namespaceelemente müssen nicht eingezogen werden.

// ✔️ OK - A is a top-level module.
module A

let function1 a b = a - b * b
// ✔️ OK - A1 and A2 are local modules.
module A1 =
    let function1 a b = a * a + b * b

module A2 =
    let function2 a b = a * a - b * b

Formatieren von do-Deklarationen

In Typdeklarationen, Moduldeklarationen und Berechnungsausdrücken ist manchmal die Verwendung von do oder do! für Vorgänge mit Nebenwirkungen erforderlich. Wenn diese sich über mehrere Zeilen erstrecken, wenden Sie auf neuen Zeilen einen Einzug an, um diesen konsistent mit let/let! zu halten. Im folgenden Beispiel wird do in einer Klasse verwendet:

// ✔️ OK
type Foo() =
    let foo =
        fooBarBaz
        |> loremIpsumDolorSitAmet
        |> theQuickBrownFoxJumpedOverTheLazyDog

    do
        fooBarBaz
        |> loremIpsumDolorSitAmet
        |> theQuickBrownFoxJumpedOverTheLazyDog

// ❌ Not OK - notice the "do" expression is indented one space less than the `let` expression
type Foo() =
    let foo =
        fooBarBaz
        |> loremIpsumDolorSitAmet
        |> theQuickBrownFoxJumpedOverTheLazyDog
    do fooBarBaz
       |> loremIpsumDolorSitAmet
       |> theQuickBrownFoxJumpedOverTheLazyDog

In diesem Beispiel wird do! mit zwei Leerzeichen als Einzug verwendet (da es bei do! zufällig keinen Unterschied gegenüber vier Leerzeichen als Einzug gibt):

// ✔️ OK
async {
  let! foo =
    fooBarBaz
    |> loremIpsumDolorSitAmet
    |> theQuickBrownFoxJumpedOverTheLazyDog

  do!
    fooBarBaz
    |> loremIpsumDolorSitAmet
    |> theQuickBrownFoxJumpedOverTheLazyDog
}

// ❌ Not OK - notice the "do!" expression is indented two spaces more than the `let!` expression
async {
  let! foo =
    fooBarBaz
    |> loremIpsumDolorSitAmet
    |> theQuickBrownFoxJumpedOverTheLazyDog
  do! fooBarBaz
      |> loremIpsumDolorSitAmet
      |> theQuickBrownFoxJumpedOverTheLazyDog
}

Formatieren von Vorgängen für Berechnungsausdrücke

Beim Erstellen benutzerdefinierter Vorgänge für Berechnungsausdrücke wird die camelCase-Benennung empfohlen:

// ✔️ OK
type MathBuilder() =
    member _.Yield _ = 0

    [<CustomOperation("addOne")>]
    member _.AddOne (state: int) =
        state + 1

    [<CustomOperation("subtractOne")>]
    member _.SubtractOne (state: int) =
        state - 1

    [<CustomOperation("divideBy")>]
    member _.DivideBy (state: int, divisor: int) =
        state / divisor

    [<CustomOperation("multiplyBy")>]
    member _.MultiplyBy (state: int, factor: int) =
        state * factor

let math = MathBuilder()

let myNumber =
    math {
        addOne
        addOne
        addOne
        subtractOne
        divideBy 2
        multiplyBy 10
    }

Die modellierte Domäne legt letztendlich die Namenskonvention fest. Wenn eine andere Konvention idiomatisch ist, sollte stattdessen diese verwendet werden.

Wenn der Rückgabewert eines Ausdrucks ein Berechnungsausdruck ist, sollten Sie den Schlüsselwortnamen des Berechnungsausdrucks auf eine eigene Zeile schreiben:

// ✔️ OK
let foo () = 
    async {
        let! value = getValue()
        do! somethingElse()
        return! anotherOperation value 
    }

Sie können auch den Berechnungsausdruck auf derselben Zeile wie den Bindungsnamen platzieren:

// ✔️ OK
let foo () = async {
    let! value = getValue()
    do! somethingElse()
    return! anotherOperation value 
}

Unabhängig von Ihren Vorlieben sollten Sie stets versuchen, in Ihrer gesamten Codebasis konsistent zu bleiben. Sie können Ihre Einstellungen dann in einem Formatierer angeben, um sie konsistent zu halten.

Formatieren von Typen und Typanmerkungen

In diesem Abschnitt wird das Formatierern von Typen und Typanmerkungen beschrieben. Dies schließt das Formatieren von Signaturdateien mit der Erweiterung .fsi ein.

Bevorzugen der Präfixsyntax für generische Typen (Foo<T>) mit einigen spezifischen Ausnahmen

F# erlaubt sowohl beim Schreibens generischer Typen den Postfixstil (z. B. int list) als auch den Präfixstil (z. B. list<int>). Der Postfixstil kann nur mit einem Typargument verwendet werden. Ziehen Sie immer den .NET-Stil vor – außer bei den folgenden fünf Typen:

  1. Verwenden Sie für F#-Listen den Postfixstil: int list anstelle von list<int>.
  2. Verwenden Sie für F#-Optionen den Postfixstil: int option anstelle von option<int>.
  3. Verwenden Sie für F#-Wertoptionen den Postfixstil: int voption anstelle von voption<int>.
  4. Verwenden Sie für F#-Arrays den Postfixstil: int array anstelle von array<int> oder int[].
  5. Verwenden Sie für Verweiszellen int ref anstelle von ref<int> oder Ref<int>.

Verwenden Sie für alle anderen Typen den Präfixstil.

Formatieren von Funktionstypen

Setzen Sie beim Definieren der Signatur einer Funktion Leerzeichen um das Symbol ->:

// ✔️ OK
type MyFun = int -> int -> string

// ❌ Not OK
type MyFunBad = int->int->string

Formatieren von Wert- und Argumenttypanmerkungen

Wenn Sie Werte oder Argumente mit Typanmerkungen definieren, verwenden Sie Leerzeichen nach dem :-Symbol, aber nicht davor:

// ✔️ OK
let complexFunction (a: int) (b: int) c = a + b + c

let simpleValue: int = 0 // Type annotation for let-bound value

type C() =
    member _.Property: int = 1

// ❌ Not OK
let complexFunctionPoorlyAnnotated (a :int) (b :int) (c:int) = a + b + c
let simpleValuePoorlyAnnotated1:int = 1
let simpleValuePoorlyAnnotated2 :int = 2

Formatieren von mehrzeiligen Typanmerkungen

Eine lange oder mehrzeilige Typanmerkung platzieren Sie auf der nächsten Zeile, die Sie um eine Ebene einziehen.

type ExprFolder<'State> =
    { exprIntercept: 
        ('State -> Expr -> 'State) -> ('State -> Expr -> 'State -> 'State -> Exp -> 'State }
        
let UpdateUI
    (model:
#if NETCOREAPP2_1
        ITreeModel
#else
        TreeModel
#endif
    )
    (info: FileInfo) =
    // code
    ()

let f
    (x:
        {|
            a: Second
            b: Metre
            c: Kilogram
            d: Ampere
            e: Kelvin
            f: Mole
            g: Candela
        |})
    =
    x.a

type Sample
    (
        input: 
            LongTupleItemTypeOneThing * 
            LongTupleItemTypeThingTwo * 
            LongTupleItemTypeThree * 
            LongThingFour * 
            LongThingFiveYow
    ) =
    class
    end

Für anonyme Inlinedatensatztypen können Sie auch den Stroustrup-Stil verwenden:

let f
    (x: {|
        x: int
        y: AReallyLongTypeThatIsMuchLongerThan40Characters
     |})
    =
    x

Formatieren von Rückgabetypanmerkungen

Verwenden Sie in Rückgabetypanmerkungen von Funktionen oder Membern Leerzeichen vor und hinter dem :-Symbol:

// ✔️ OK
let myFun (a: decimal) b c : decimal = a + b + c

type C() =
    member _.SomeMethod(x: int) : int = 1

// ❌ Not OK
let myFunBad (a: decimal) b c:decimal = a + b + c

let anotherFunBad (arg: int): unit = ()

type C() =
    member _.SomeMethodBad(x: int): int = 1

Formatieren von Typen in Signaturen

Beim Schreiben vollständiger Funktionstypen in Signaturen ist es manchmal erforderlich, die Argumente auf mehrere Zeilen aufzuteilen. Der Rückgabetyp wird immer eingezogen.

Bei einer Tupelfunktion werden die Argumente durch * am Ende jeder Zeile getrennt.

Angenommen, Sie haben eine Funktion mit der folgenden Implementierung:

let SampleTupledFunction(arg1, arg2, arg3, arg4) = ...

In der zugehörigen Signaturdatei (Erweiterung .fsi) kann die Funktion wie folgt formatiert werden, wenn eine mehrzeilige Formatierung erforderlich ist:

// ✔️ OK
val SampleTupledFunction:
    arg1: string *
    arg2: string *
    arg3: int *
    arg4: int ->
        int list

Sehen Sie sich auch diese zusammengesetzte Funktion an:

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

In der zugehörigen Signaturdatei werden die -> am Ende jeder Zeile platziert:

// ✔️ OK
val SampleCurriedFunction:
    arg1: string ->
    arg2: string ->
    arg3: int ->
    arg4: int ->
        int list

Gehen Sie von eine Funktion aus, die eine Mischung aus zusammengesetzten und Tupelargumenten akzeptiert:

// Typical call syntax:
let SampleMixedFunction
        (arg1, arg2)
        (arg3, arg4, arg5)
        (arg6, arg7)
        (arg8, arg9, arg10) = ..

In der zugehörigen Signaturdatei werden die Typen, denen ein Tupel vorangestellt ist, eingezogen.

// ✔️ OK
val SampleMixedFunction:
    arg1: string *
    arg2: string ->
        arg3: string *
        arg4: string *
        arg5: TType ->
            arg6: TType *
            arg7: TType ->
                arg8: TType *
                arg9: TType *
                arg10: TType ->
                    TType list

Diese Regeln gelten auch für Member in Typsignaturen:

type SampleTypeName =
    member ResolveDependencies:
        arg1: string *
        arg2: string ->
            string

Formatieren expliziter generischer Typargumente und Einschränkungen

Die folgenden Richtlinien gelten für Funktionsdefinitionen, Memberdefinitionen, Typdefinitionen und Funktionsanwendungen.

Schreiben Sie generische Typargumente und Einschränkungen auf einer einzelnen Zeile, sofern sie nicht zu lang sind:

// ✔️ OK
let f<'T1, 'T2 when 'T1: equality and 'T2: comparison> param =
    // function body

Wenn die generische Typargumente/Einschränkungen nicht zusammen mit den Funktionsparametern auf eine Zeile passen, aber die Typparameter/Einschränkungen allein, platzieren Sie die Parameter auf neuen Zeilen:

// ✔️ OK
let f<'T1, 'T2 when 'T1: equality and 'T2: comparison>
    param
    =
    // function body

Wenn die Typparameter oder Einschränkungen zu lang sind, unterbrechen Sie sie und richten sie wie unten gezeigt aus. Platzieren Sie die Liste der Typparameter unabhängig von ihrer Länge auf derselben Zeile wie die Funktion. Bei Einschränkungen platzieren Sie when auf der ersten Zeile, und schreiben Sie jede Einschränkung unabhängig von ihrer Länge auf einer eigenen Zeile bei. Setzen Sie am Ende der letzten Zeile ein >-Zeichen. Ziehen Sie die Einschränkungen um eine Ebene ein.

// ✔️ OK
let inline f< ^T1, ^T2
    when ^T1: (static member Foo1: unit -> ^T2)
    and ^T2: (member Foo2: unit -> int)
    and ^T2: (member Foo3: string -> ^T1 option)>
    arg1
    arg2
    =
    // function body

Wenn die Typparameter/Einschränkungen aufgeteilt, aber keine normalen Funktionsparameter vorhanden sind, platzieren Sie das =-Zeichen auf einer neuen Zeile:

// ✔️ OK
let inline f< ^T1, ^T2
    when ^T1: (static member Foo1: unit -> ^T2)
    and ^T2: (member Foo2: unit -> int)
    and ^T2: (member Foo3: string -> ^T1 option)>
    =
    // function body

Für Funktionsanwendungen gelten dieselben Regeln:

// ✔️ OK
myObj
|> Json.serialize<
    {| child: {| displayName: string; kind: string |}
       newParent: {| id: string; displayName: string |}
       requiresApproval: bool |}>

// ✔️ OK
Json.serialize<
    {| child: {| displayName: string; kind: string |}
       newParent: {| id: string; displayName: string |}
       requiresApproval: bool |}>
    myObj

Formatieren der Vererbung

Die Argumente für den Basisklassenkonstruktor sind in der Argumentliste in der inherit-Klausel enthalten. Platzieren Sie die inherit-Klausel auf einer neuen Zeile, die um eine Ebene eingezogen ist.

type MyClassBase(x: int) =
   class
   end

// ✔️ OK
type MyClassDerived(y: int) =
   inherit MyClassBase(y * 2)

// ❌ Not OK
type MyClassDerived(y: int) = inherit MyClassBase(y * 2)

Einen lange oder mehrzeiligen Konstruktor platzieren Sie auf der nächsten Zeile, die Sie um eine Ebene einziehen.
Formatieren Sie solche mehrzeiligen Konstruktoren gemäß den Regeln für mehrzeilige Funktionsanwendungen.

type MyClassBase(x: string) =
   class
   end

// ✔️ OK
type MyClassDerived(y: string) =
    inherit 
        MyClassBase(
            """
            very long
            string example
            """
        )
        
// ❌ Not OK
type MyClassDerived(y: string) =
    inherit MyClassBase(
        """
        very long
        string example
        """)

Formatierung des primären Konstruktors

In Standardformatkonventionen wird zwischen dem Typnamen und den Klammern für den primären Konstruktor kein Leerzeichen hinzugefügt.

// ✔️ OK
type MyClass() =
    class
    end

type MyClassWithParams(x: int, y: int) =
    class
    end
        
// ❌ Not OK
type MyClass () =
    class
    end

type MyClassWithParams (x: int, y: int) =
    class
    end

Mehrere Konstruktoren

Wenn die inherit-Klausel Teil eines Datensatzes und kurz ist, platzieren Sie sie auf derselben Zeile. Wenn sie hingegen lang oder mehrzeilig ist, platzieren Sie sie auf der nächsten Zeile um eine Ebene eingezogen.

type BaseClass =
    val string1: string
    new () = { string1 = "" }
    new (str) = { string1 = str }

type DerivedClass =
    inherit BaseClass

    val string2: string
    new (str1, str2) = { inherit BaseClass(str1); string2 = str2 }
    new () = 
        { inherit 
            BaseClass(
                """
                very long
                string example
                """
            )
          string2 = str2 }

Formatieren von Attributen

Attribute werden über einem Konstrukt angeordnet:

// ✔️ OK
[<SomeAttribute>]
type MyClass() = ...

// ✔️ OK
[<RequireQualifiedAccess>]
module M =
    let f x = x

// ✔️ OK
[<Struct>]
type MyRecord =
    { Label1: int
      Label2: string }

Sie sollten hinter der XML-Dokumentation stehen:

// ✔️ OK

/// Module with some things in it.
[<RequireQualifiedAccess>]
module M =
    let f x = x

Formatieren von Attributen in Parametern

Attribute können auch in Parametern platziert werden. In diesem Fall platzieren Sie sie auf derselben Zeile wie der Parameter und vor dem Namen:

// ✔️ OK - defines a class that takes an optional value as input defaulting to false.
type C() =
    member _.M([<Optional; DefaultParameterValue(false)>] doSomething: bool)

Formatieren mehrerer Attribute

Wenn auf ein Konstrukt, das kein Parameter ist, mehrere Attribute angewandt werden, platzieren Sie diese einzeln auf separaten Zeilen:

// ✔️ OK

[<Struct>]
[<IsByRefLike>]
type MyRecord =
    { Label1: int
      Label2: string }

Wenn sie auf einen Parameter angewandt werden, platzieren Sie die Attribute auf derselben Zeile und trennen sie durch ein ;-Zeichen ab.

Danksagungen

Diese Richtlinien basieren auf A comprehensive guide to F# Formatting Conventions (Umfassender Leitfaden zu F#-Formatierungskonventionen) von Anh-Dung Phan.