Aracılığıyla paylaş


F# kodlama kuralları

Aşağıdaki kurallar, büyük F# kod temelleriyle çalışma deneyiminden formüle edilir. İyi F# kodunun Beş ilkesi, her önerinin temelini oluşturur. Bunlar F# bileşeni tasarım yönergeleriyle ilgilidir, ancak yalnızca kitaplıklar gibi bileşenler için değil, tüm F# kodları için geçerlidir.

Kod düzenleme

F# kodu düzenlemenin iki birincil yolunu içerir: modüller ve ad alanları. Bunlar benzerdir, ancak aşağıdaki farklılıklara sahiptir:

  • Ad alanları .NET ad alanları olarak derlenir. Modüller statik sınıflar olarak derlenir.
  • Ad alanları her zaman en üst düzeydir. Modüller üst düzey olabilir ve diğer modüllerin içinde iç içe yerleştirilebilir.
  • Ad alanları birden çok dosyaya yayılabilir. Modüller yapamaz.
  • Modüller ve [<AutoOpen>]ile [<RequireQualifiedAccess>] dekore edilebilir.

Aşağıdaki yönergeler, kodunuzu düzenlemek için bunları kullanmanıza yardımcı olur.

Ad alanlarını en üst düzeyde tercih edin

Genel kullanıma açık tüm kodlar için ad alanları, en üst düzeydeki modüller için tercih edilir. .NET ad alanları olarak derlendiğinden, C# ile kullanılmadan using statickullanılabilir.

// Recommended.
namespace MyCode

type MyClass() =
    ...

Üst düzey modülün kullanılması yalnızca F# dilinden çağrıldığında farklı görünmeyebilir, ancak C# tüketicileri için, belirli using static bir C# yapısının farkında olmadığında modüle MyCode uygun MyClass olmak zorunda kalarak arayanlar şaşırabilir.

// Will be seen as a static class outside F#
module MyCode

type MyClass() =
    ...

Dikkatle uygulama [<AutoOpen>]

Yapı [<AutoOpen>] çağıranların kullanabildiği şeylerin kapsamını kirletebilir ve bir şeyin nereden geldiğinin yanıtı "büyü"dür. Bu iyi bir şey değil. Bu kuralın bir istisnası F# Çekirdek Kitaplığı'nın kendisidir (ancak bu durum biraz tartışmalıdır).

Ancak, ortak API'den ayrı olarak düzenlemek istediğiniz bir genel API için yardımcı işlevselliğiniz varsa kolaylık sağlar.

module MyAPI =
    [<AutoOpen>]
    module private Helpers =
        let helper1 x y z =
            ...

    let myFunction1 x =
        let y = ...
        let z = ...

        helper1 x y z

Bu, her çağırdığınızda bir yardımcıyı tam olarak nitelemeden uygulama ayrıntılarını bir işlevin genel API'sinden temiz bir şekilde ayırmanızı sağlar.

Ayrıca, uzantı yöntemlerinin ve ifade oluşturucularının ad alanı düzeyinde kullanıma alınması ile [<AutoOpen>]düzgün bir şekilde ifade edilebilir.

Adların çakışabileceği veya okunabilirliğe yardımcı olduğunu hissettiğiniz her durumda kullanın [<RequireQualifiedAccess>]

özniteliğini [<RequireQualifiedAccess>] bir modüle eklemek, modülün açılmayabileceğini ve modülün öğelerine yapılan başvuruların açık nitelikli erişim gerektirdiğini gösterir. Örneğin modülde Microsoft.FSharp.Collections.List bu öznitelik bulunur.

Bu, modüldeki işlevlerin ve değerlerin diğer modüllerdeki adlarla çakışma olasılığı olan adlara sahip olması durumunda kullanışlıdır. Nitelikli erişim gerektirmek, uzun süreli bakım ve bir kitaplığın gelişme becerisini büyük ölçüde artırabilir.

[<RequireQualifiedAccess>]
module StringTokenization =
    let parse s = ...

...

let s = getAString()
let parsed = StringTokenization.parse s // Must qualify to use 'parse'

Deyimleri topolojik olarak sıralama open

F# dilinde, bildirimlerin sırası, deyimlerle open (ve open type) yalnızca daha aşağısı da open dahil olmak üzere önemlidir. Bu, ve using static etkisinin using bir dosyadaki bu deyimlerin sıralanmasından bağımsız olduğu C# dilinden farklı bir durumdur.

F# dilinde, bir kapsama açılan öğeler zaten mevcut olan diğer öğeleri gölgeleyebilir. Bu, deyimleri yeniden sıralamanın open kodun anlamını değiştirebileceği anlamına gelir. Sonuç olarak, tüm open deyimlerin (örneğin alfasayısal olarak) rastgele sıralanması önerilmez, çünkü bekleyebileceğiniz farklı davranışlar oluşturursunuz.

Bunun yerine, bunları topolojik olarak sıralamanızı öneririz; yani, deyimlerinizi open sisteminizin katmanlarının tanımlandığı sırayla sıralayın. Alfasayısal sıralamanın farklı topolojik katmanlar içinde yapılması da göz önünde bulundurulabilir.

Örneğin, F# derleyici hizmeti genel API dosyası için topolojik sıralama aşağıda verilmiştir:

namespace Microsoft.FSharp.Compiler.SourceCodeServices

open System
open System.Collections.Generic
open System.Collections.Concurrent
open System.Diagnostics
open System.IO
open System.Reflection
open System.Text

open FSharp.Compiler
open FSharp.Compiler.AbstractIL
open FSharp.Compiler.AbstractIL.Diagnostics
open FSharp.Compiler.AbstractIL.IL
open FSharp.Compiler.AbstractIL.ILBinaryReader
open FSharp.Compiler.AbstractIL.Internal
open FSharp.Compiler.AbstractIL.Internal.Library

open FSharp.Compiler.AccessibilityLogic
open FSharp.Compiler.Ast
open FSharp.Compiler.CompileOps
open FSharp.Compiler.CompileOptions
open FSharp.Compiler.Driver

open Internal.Utilities
open Internal.Utilities.Collections

Çizgi sonu topolojik katmanları birbirinden ayırır ve her katman daha sonra alfasayısal olarak sıralanır. Bu, değerleri yanlışlıkla gölgelendirmeden kodu temiz bir şekilde düzenler.

Yan etkileri olan değerleri içeren sınıfları kullanma

Bir değeri başlatmanın, bir veritabanına veya başka bir uzak kaynağa bağlam örneği oluşturma gibi yan etkileri olabilir. Bu tür şeyleri bir modülde başlatmak ve sonraki işlevlerde kullanmak caziptir:

// Not recommended, side-effect at static initialization
module MyApi =
    let dep1 = File.ReadAllText "/Users/<name>/config-options.txt"
    let dep2 = Environment.GetEnvironmentVariable "DEP_2"

    let private r = Random()
    let dep3() = r.Next() // Problematic if multiple threads use this

    let function1 arg = doStuffWith dep1 dep2 dep3 arg
    let function2 arg = doStuffWith dep1 dep2 dep3 arg

Bu durum genellikle birkaç nedenden dolayı sorunludur:

İlk olarak, uygulama yapılandırması ve dep2ile dep1 kod tabanına gönderiliyor. Bunun daha büyük kod temellerinde tutulması zordur.

İkinci olarak, statik olarak başlatılan veriler, bileşeniniz birden çok iş parçacığı kullanacaksa iş parçacığı güvenli olmayan değerler içermemelidir. Bu açıkça tarafından dep3ihlal edilir.

Son olarak, modül başlatma tüm derleme birimi için statik bir oluşturucuya derlenir. Bu modülde let-bound değer başlatma işleminde herhangi bir hata oluşursa, uygulamanın tüm ömrü boyunca önbelleğe alınan bir TypeInitializationException hata olarak gösterilir. Bunu tanılamak zor olabilir. Genellikle mantık yürütmeye çalışabileceğiniz bir iç özel durum vardır, ancak yoksa, kök nedenin ne olduğunu söylemek yoktur.

Bunun yerine, bağımlılıkları tutmak için basit bir sınıf kullanmanız gerekir:

type MyParametricApi(dep1, dep2, dep3) =
    member _.Function1 arg1 = doStuffWith dep1 dep2 dep3 arg1
    member _.Function2 arg2 = doStuffWith dep1 dep2 dep3 arg2

Bu, aşağıdakileri etkinleştirir:

  1. Bağımlı durumları API'nin dışına gönderme.
  2. Yapılandırma artık API'nin dışında yapılabilir.
  3. Bağımlı değerler için başlatma hatalarının olarak TypeInitializationExceptionbildirilme olasılığı yüksek değildir.
  4. API'yi test etmek artık daha kolay.

Hata yönetimi

Büyük sistemlerde hata yönetimi karmaşık ve nüanslı bir çabadır ve sistemlerinizin hataya dayanıklı olduğundan ve iyi davranıldığından emin olmak için gümüş madde işareti yoktur. Aşağıdaki yönergeler bu zor alanda gezinme konusunda rehberlik sunmalıdır.

Etki alanınıza ait türlerde hata durumlarını ve geçersiz durumu temsil etme

Ayrımcı Birleşimler ile F# size tür sisteminizdeki hatalı program durumunu temsil etme olanağı sağlar. Örneğin:

type MoneyWithdrawalResult =
    | Success of amount:decimal
    | InsufficientFunds of balance:decimal
    | CardExpired of DateTime
    | UndisclosedFailure

Bu durumda, bir banka hesabından para çekmenin başarısız olabileceğinin bilinen üç yolu vardır. Her hata olayı türünde temsil edilir ve bu nedenle program boyunca güvenli bir şekilde ele alınabilir.

let handleWithdrawal amount =
    let w = withdrawMoney amount
    match w with
    | Success am -> printfn $"Successfully withdrew %f{am}"
    | InsufficientFunds balance -> printfn $"Failed: balance is %f{balance}"
    | CardExpired expiredDate -> printfn $"Failed: card expired on {expiredDate}"
    | UndisclosedFailure -> printfn "Failed: unknown"

Genel olarak, etki alanınızda bir şeyin başarısız olabileceği farklı yöntemleri modelleyebilirseniz, hata işleme kodu artık normal program akışına ek olarak ilgilenmeniz gereken bir şey olarak değerlendirilmez. Yalnızca normal program akışının bir parçasıdır ve istisnai olarak kabul edilmez. Bunun iki temel avantajı vardır:

  1. Etki alanınız zaman içinde değiştikçe bakımını yapmak daha kolaydır.
  2. Hata durumlarının birim testi daha kolaydır.

Hatalar türlerle temsil edilemediğinde özel durumları kullanma

Tüm hatalar bir sorun etki alanında gösterilemiyor. Bu tür hatalar doğası gereği istisnaidir, bu nedenle F# dilinde özel durumlar oluşturup yakalama yeteneği.

İlk olarak, Özel Durum Tasarımı Yönergeleri'ni okumanız önerilir. Bunlar F# için de geçerlidir.

Özel durum oluşturma amacıyla F# dilinde kullanılabilen ana yapılar aşağıdaki tercih sırasına göre değerlendirilmelidir:

İşlev Sözdizimi Purpose
nullArg nullArg "argumentName" Belirtilen bağımsız değişken adıyla bir System.ArgumentNullException oluşturur.
invalidArg invalidArg "argumentName" "message" Belirtilen bağımsız değişken adı ve iletisiyle bir System.ArgumentException oluşturur.
invalidOp invalidOp "message" Belirtilen iletiyle bir System.InvalidOperationException oluşturur.
raise raise (ExceptionType("message")) Özel durumlar oluşturmak için genel amaçlı mekanizma.
failwith failwith "message" Belirtilen iletiyle bir System.Exception oluşturur.
failwithf failwithf "format string" argForFormatString biçim dizesi ve onun girişleri tarafından belirlenen bir ileti ile bir System.Exception oluşturur.

, invalidArgve invalidOp uygun olduğunda oluşturma ArgumentNullExceptionmekanizması olarak , ArgumentExceptionve InvalidOperationException kullanınnullArg.

failwith ve failwithf işlevleri genellikle, belirli bir özel durum değil, temel Exception türü yükselttiğinden kaçınılmalıdır. Özel Durum Tasarım Yönergeleri'ne göre, daha belirli özel durumlar tetikleyebilirsiniz.

Özel durum işleme söz dizimlerini kullanma

F# söz dizimi aracılığıyla özel durum desenlerini try...with destekler:

try
    tryGetFileContents()
with
| :? System.IO.FileNotFoundException as e -> // Do something with it here
| :? System.Security.SecurityException as e -> // Do something with it here

Kodu temiz tutmak istiyorsanız, desen eşleştirme ile bir özel durum karşısında gerçekleştirilecek işlevselliğin mutabık tutulması biraz karmaşık olabilir. Bunu işlemenin böyle bir yolu, bir hata olayını çevreleyen işlevselliği özel durumla gruplandırmak için bir araç olarak etkin desenleri kullanmaktır. Örneğin, özel durum oluştururken değerli bilgileri özel durum meta verilerine alan bir API'yi tüketiyor olabilirsiniz. Etkin Desen içindeki yakalanan özel durumun gövdesindeki yararlı bir değerin işaretini kaldırıp bu değeri döndürmek bazı durumlarda yararlı olabilir.

Özel durumları değiştirmek için monadik hata işleme kullanmayın

Özel durumlar genellikle saf işlevsel paradigmada tabu olarak görülür. Aslında, özel durumlar saflığı ihlal eder, bu nedenle onları işlevsel olarak saf değil olarak değerlendirmek güvenlidir. Ancak bu, kodun nerede çalıştırılması gerektiğinin gerçekliğini yoksayar ve çalışma zamanı hataları oluşabilir. Genel olarak, hoş olmayan sürprizleri en aza indirmek (C# dilinde boş catch veya yığın izlemesini yanlış yönetmek, bilgileri atmak gibi) için çoğu şeyin saf veya toplam olmadığını varsayarak kod yazın.

Bir bütün olarak .NET çalışma zamanı ve diller arası ekosistemdeki ilgi ve uygunluk açısından Özel Durumların aşağıdaki temel güçlü yönlerini/yönlerini göz önünde bulundurmak önemlidir:

  • Bunlar, bir sorunda hata ayıklama sırasında yararlı olan ayrıntılı tanılama bilgileri içerir.
  • Çalışma zamanı ve diğer .NET dilleri tarafından iyi anlaşılır.
  • Semantiğin bazı alt kümelerini geçici olarak uygulayarak özel durumlardan kaçınmanın yolunun dışına çıkan kodlarla karşılaştırıldığında önemli bir ortaklığı azaltabilirler.

Bu üçüncü nokta kritik öneme sahiptir. Karmaşık olmayan işlemler için özel durumların kullanılmaması aşağıdaki gibi yapılarla ilgilenmeye neden olabilir:

Result<Result<MyType, string>, string list>

Bu, "dizeli yazılan" hatalarda desen eşleştirme gibi kırılgan kodlara kolayca yol açabilir:

let result = doStuff()
match result with
| Ok r -> ...
| Error e ->
    if e.Contains "Error string 1" then ...
    elif e.Contains "Error string 2" then ...
    else ... // Who knows?

Ayrıca, "daha güzel" bir tür döndüren "basit" işlev isteğinde herhangi bir özel durumu yutmak cazip olabilir:

// Can be problematic due to discarding the cause of error.
let tryReadAllText (path : string) =
    try System.IO.File.ReadAllText path |> Some
    with _ -> None

Ne yazık ki, tryReadAllText bir dosya sisteminde gerçekleşen çok sayıda şeye bağlı olarak çok sayıda özel durum oluşturabilir ve bu kod, ortamınızda gerçekten neyin yanlış gittiğiyle ilgili tüm bilgileri atar. Bu kodu bir sonuç türüyle değiştirirseniz, "dize türünde" hata iletisini ayrıştırmaya dönersiniz:

// Problematic, callers only have a string to figure the cause of error.
let tryReadAllText (path : string) =
    try System.IO.File.ReadAllText path |> Ok
    with e -> Error e.Message

let r = tryReadAllText "path-to-file"
match r with
| Ok text -> ...
| Error e ->
    if e.Contains "uh oh, here we go again..." then ...
    else ...

Özel durum nesnesinin kendisini oluşturucuya Error yerleştirmek, sizi işlev yerine çağrı sitesinde özel durum türüyle düzgün şekilde ilgilenmeye zorlar. Bunu yapmak, bir API'nin çağıranı olarak ilgilenmek için kötü şöhretli olmayan denetlenmiş özel durumlar oluşturur.

Yukarıdaki örneklere iyi bir alternatif, belirli özel durumları yakalamak ve bu özel durum bağlamında anlamlı bir değer döndürmektir. İşlevi tryReadAllText aşağıdaki gibi değiştirirseniz, None daha fazla anlamı vardır:

let tryReadAllTextIfPresent (path : string) =
    try System.IO.File.ReadAllText path |> Some
    with :? FileNotFoundException -> None

Bu işlev artık tümünü yakala işlevi yerine dosya bulunamadığında olayı düzgün bir şekilde işleyecek ve bu anlamı bir dönüşe atayacaktır. Bu dönüş değeri, bağlamsal bilgileri atarak veya çağıranları kodda ilgili olmayan bir servis talebiyle ilgilenmeye zorlamadan bu hata olayıyla eşlenebilir.

gibi Result<'Success, 'Error> türler iç içe yerleştirilmeyen temel işlemler için uygundur ve isteğe bağlı F# türleri, bir şeyin ne zaman bir şey döndürebileceğini veya hiçbir şey döndürmeyebileceğini göstermek için mükemmeldir. Ancak bunlar özel durumların yerini almayabilir ve özel durumları değiştirme girişiminde kullanılmamalıdır. Bunun yerine, özel durum ve hata yönetimi ilkesinin belirli yönlerini hedeflenen yollarla ele almak için bunlar judiciously uygulanmalıdır.

Kısmi uygulama ve noktasız programlama

F# kısmi uygulamayı ve dolayısıyla noktasız stilde programlamanın çeşitli yollarını destekler. Bu, bir modül içinde kod yeniden kullanımı veya bir şeyin uygulanması için yararlı olabilir, ancak genel kullanıma sunulacak bir şey değildir. Genel olarak, noktasız programlama kendi içinde ve kendi içinde bir erdem değildir ve stile dalmış olmayan kişiler için önemli bir bilişsel engel oluşturabilir.

Genel API'lerde kısmi uygulama ve currying kullanmayın

Çok az istisna dışında, kısmi uygulamanın genel API'lerde kullanılması tüketiciler için kafa karıştırıcı olabilir. letF# kodundaki -bound değerleri genellikle işlev değerleri değil değerlerdir. Değerleri ve işlev değerlerini bir araya getirmek, özellikle de işlev oluşturma gibi >> işleçlerle birleştirildiğinde, oldukça fazla bilişsel ek yük karşılığında birkaç kod satırı kaydedilmesine neden olabilir.

Noktasız programlama için araç etkilerini göz önünde bulundurun

Curried işlevleri bağımsız değişkenlerini etiketlemez. Bunun araç etkileri vardır. Aşağıdaki iki işlevi göz önünde bulundurun:

let func name age =
    printfn $"My name is {name} and I am %d{age} years old!"

let funcWithApplication =
    printfn "My name is %s and I am %d years old!"

Her ikisi de geçerli işlevlerdir, ancak funcWithApplication curried işlevidir. Düzenleyicide türlerinin üzerine geldiğinizde şunu görürsünüz:

val func : name:string -> age:int -> unit

val funcWithApplication : (string -> int -> unit)

Arama sitesinde, Visual Studio gibi araçlardaki araç ipuçları size tür imzası verir, ancak tanımlı ad olmadığından adları görüntülemez. Adlar iyi API tasarımı açısından kritik öneme sahiptir çünkü arayanların API'nin arkasındaki anlamı daha iyi anlamalarına yardımcı olur. Genel API'de noktasız kod kullanmak, çağıranların anlamasını zorlaştırabilir.

Genel kullanıma açık gibi funcWithApplication noktasız kodlarla karşılaşırsanız, araçların bağımsız değişkenler için anlamlı adları alabilmesi için tam bir η genişletmesi yapmanız önerilir.

Ayrıca, noktasız kodda hata ayıklamak imkansız değilse zor olabilir. Hata ayıklama araçları, yürütmenin ortasında ara değerleri inceleyebilmeniz için adlara (örneğin bağlamalara let ) bağlı değerleri kullanır. Kodunuzun denetlenecek değeri olmadığında, hata ayıklamak için hiçbir şey yoktur. Gelecekte, hata ayıklama araçları bu değerleri daha önce yürütülen yollara göre sentezlemek için gelişebilir, ancak olası hata ayıklama işlevselliğine ilişkin bahislerinizi hedge etmek iyi bir fikir değildir.

Kısmi uygulamayı iç ortak yapıyı azaltmaya yönelik bir teknik olarak düşünün

Önceki noktadan farklı olarak kısmi uygulama, bir uygulamanın içindeki veya bir API'nin daha derin iç bileşenlerini azaltmaya yönelik harika bir araçtır. Daha karmaşık API'lerin uygulanmasının birim testi için yararlı olabilir, burada ortak genellikle baş edilmesi gereken bir sorundur. Örneğin, aşağıdaki kod, bu tür bir çerçeveye dış bağımlılık uygulamadan ve ilgili bir özet API'yi öğrenmek zorunda kalmadan en sahte çerçevelerin size neler sunabileceğini gösterir.

Örneğin, aşağıdaki çözüm topografisini göz önünde bulundurun:

MySolution.sln
|_/ImplementationLogic.fsproj
|_/ImplementationLogic.Tests.fsproj
|_/API.fsproj

ImplementationLogic.fsproj aşağıdakiler gibi kodu kullanıma sunar:

module Transactions =
    let doTransaction txnContext txnType balance =
        ...

type Transactor(ctx, currentBalance) =
    member _.ExecuteTransaction(txnType) =
        Transactions.doTransaction ctx txnType currentBalance
        ...

'da ImplementationLogic.Tests.fsproj birim testi Transactions.doTransaction kolaydır:

namespace TransactionsTestingUtil

open Transactions

module TransactionsTestable =
    let getTestableTransactionRoutine mockContext = Transactions.doTransaction mockContext

Bir sahte bağlam nesnesiyle kısmen uygulamak doTransaction , her seferinde sahte bir bağlam oluşturmak zorunda kalmadan tüm birim testlerinizde işlevi çağırmanıza olanak tanır:

module TransactionTests

open Xunit
open TransactionTypes
open TransactionsTestingUtil
open TransactionsTestingUtil.TransactionsTestable

let testableContext =
    { new ITransactionContext with
        member _.TheFirstMember() = ...
        member _.TheSecondMember() = ... }

let transactionRoutine = getTestableTransactionRoutine testableContext

[<Fact>]
let ``Test withdrawal transaction with 0.0 for balance``() =
    let expected = ...
    let actual = transactionRoutine TransactionType.Withdraw 0.0
    Assert.Equal(expected, actual)

Bu tekniği tüm kod tabanınıza evrensel olarak uygulamayın, ancak karmaşık iç bileşenler ve bu iç işlevlerin birim testi için ortak değeri azaltmanın iyi bir yoludur.

Erişim denetimi

F# erişim denetimi için birden çok seçeneğe sahiptir ve .NET çalışma zamanındaki kullanılabilir seçeneklerden devralınır. Bunlar yalnızca türler için kullanılamaz; işlevler için de kullanabilirsiniz.

Yaygın olarak kullanılan kitaplıklar bağlamında iyi uygulamalar:

  • public Genel olarak kullanılabilir olmalarına ihtiyaç duyana kadar türleri ve üyeleri tercih edin. Bu, tüketicilerin neyle eşleştiğinden de en aza indirir.
  • Tüm yardımcı işlevselliğini privatekorumak için çaba gösterin.
  • Çok sayıda yardımcı işleve dönüşen özel bir modülde kullanımını [<AutoOpen>] göz önünde bulundurun.

Tür çıkarımı ve genel değerler

Tür çıkarımı, çok fazla ortak yazı yazmanızı engelleyebilir. Ayrıca F# derleyicisindeki otomatik genelleştirme, sizin için neredeyse hiç fazla çaba harcamadan daha genel kod yazmanıza yardımcı olabilir. Ancak, bu özellikler evrensel olarak iyi değildir.

  • Bağımsız değişken adlarını genel API'lerde açık türlerle etiketlemeyi göz önünde bulundurun ve bunun için tür çıkarımına güvenmeyin.

    Bunun nedeni, derleyicinin değil API'nizin şeklinin denetiminde olmanız gerekir. Derleyici sizin için türleri çıkarsama konusunda iyi bir iş çıkarasa da, bağlı olduğu iç bileşenler türleri değiştirdiyse API'nizin şeklinin değiştirilmesi mümkündür. İstediğiniz şey bu olabilir, ancak aşağı akış tüketicilerinin daha sonra ilgilenmesi gereken hataya neden olan API değişikliğine neredeyse kesin olarak neden olur. Bunun yerine, genel API'nizin şeklini açıkça denetlerseniz, bu hataya neden olan değişiklikleri denetleyebilirsiniz. DDD açısından, bu bir Bozulma önleme katmanı olarak düşünülebilir.

  • Genel bağımsız değişkenlerinize anlamlı bir ad vermeyi göz önünde bulundurun.

    Belirli bir etki alanına özgü olmayan gerçekten genel bir kod yazmadığınız sürece, anlamlı bir ad diğer programcıların üzerinde çalıştıkları etki alanını anlamalarına yardımcı olabilir. Örneğin, belge veritabanıyla etkileşim kurma bağlamında adlı 'Document bir tür parametresi, genel belge türlerinin çalıştığınız işlev veya üye tarafından kabul edilebileceğini daha net hale getirir.

  • Genel tür parametrelerini PascalCase ile adlandırmayı göz önünde bulundurun.

    .NET'te işlem yapmak için genel yöntem budur, bu nedenle snake_case veya camelCase yerine PascalCase kullanmanız önerilir.

Son olarak otomatik genelleştirme, F# veya büyük bir kod tabanında yeni olan kişiler için her zaman bir boon değildir. Genel bileşenlerin kullanılmasında bilişsel ek yük vardır. Ayrıca, otomatik olarak genelleştirilmiş işlevler farklı giriş türleriyle kullanılmıyorsa (bu şekilde kullanılması amaçlanıyorsa bırakın), genel olmalarının gerçek bir avantajı yoktur. Yazdığınız kodun genel olmanın gerçekten yararlı olup olmadığını her zaman göz önünde bulundurun.

Performans

Yüksek ayırma oranlarına sahip küçük türler için yapıları göz önünde bulundurun

Yapıların (Değer Türleri olarak da adlandırılır) kullanılması genellikle nesneleri ayırmayı önlediğinden bazı kodlarda daha yüksek performansa neden olabilir. Ancak yapılar her zaman "daha hızlı git" düğmesi değildir: Yapıdaki verilerin boyutu 16 baytı aşarsa, verilerin kopyalanması genellikle başvuru türünü kullanmaktan daha fazla CPU süresi harcaması ile sonuçlanabilir.

Bir yapı kullanmanız gerekip gerekmediğini belirlemek için aşağıdaki koşulları göz önünde bulundurun:

  • Verilerinizin boyutu 16 bayt veya daha küçükse.
  • Çalışan bir programda bellekte bu türlerin birçok örneğinin yerleşik olarak yer almış olma olasılığınız varsa.

İlk koşul geçerliyse, genellikle bir yapı kullanmanız gerekir. Her ikisi de geçerliyse, neredeyse her zaman bir yapı kullanmanız gerekir. Önceki koşulların geçerli olduğu bazı durumlar olabilir, ancak yapı kullanmak başvuru türünü kullanmaktan daha iyi veya daha kötü değildir, ancak bunlar nadir olabilir. Ancak bu gibi değişiklikler yaparken her zaman ölçüm yapmak ve varsayım veya sezgi üzerinde çalışmamak önemlidir.

Yüksek ayırma oranlarına sahip küçük değer türlerini gruplandırırken yapı demetlerini göz önünde bulundurun

Aşağıdaki iki işlevi göz önünde bulundurun:

let rec runWithTuple t offset times =
    let offsetValues x y z offset =
        (x + offset, y + offset, z + offset)

    if times <= 0 then
        t
    else
        let (x, y, z) = t
        let r = offsetValues x y z offset
        runWithTuple r offset (times - 1)

let rec runWithStructTuple t offset times =
    let offsetValues x y z offset =
        struct(x + offset, y + offset, z + offset)

    if times <= 0 then
        t
    else
        let struct(x, y, z) = t
        let r = offsetValues x y z offset
        runWithStructTuple r offset (times - 1)

Bu işlevleri BenchmarkDotNet gibi bir istatistiksel karşılaştırma aracıyla kıyasladığınızda, yapı demetlerini kullanan işlevin runWithStructTuple %40 daha hızlı çalıştığını ve bellek ayırmadığını göreceksiniz.

Ancak bu sonuçlar her zaman kendi kodunuzda geçerli olmaz. bir işlevi olarak inlineişaretlerseniz, başvuru demetlerini kullanan kod bazı ek iyileştirmeler alabilir veya ayıracak kod basitçe iyileştirilebilir. Performans söz konusu olduğunda sonuçları her zaman ölçmeli ve hiçbir zaman varsayıma veya sezgiye göre çalışmamalısınız.

Tür küçük olduğunda ve yüksek ayırma oranlarına sahip olduğunda yapı kayıtlarını göz önünde bulundurun

Daha önce açıklanan başparmak kuralı F# kayıt türleri için de geçerli olur. Bunları işleyen aşağıdaki veri türlerini ve işlevleri göz önünde bulundurun:

type Point = { X: float; Y: float; Z: float }

[<Struct>]
type SPoint = { X: float; Y: float; Z: float }

let rec processPoint (p: Point) offset times =
    let inline offsetValues (p: Point) offset =
        { p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }

    if times <= 0 then
        p
    else
        let r = offsetValues p offset
        processPoint r offset (times - 1)

let rec processStructPoint (p: SPoint) offset times =
    let inline offsetValues (p: SPoint) offset =
        { p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }

    if times <= 0 then
        p
    else
        let r = offsetValues p offset
        processStructPoint r offset (times - 1)

Bu, önceki tanımlama grubu koduna benzer, ancak bu kez örnek kayıtları ve bir iç iç işlevi kullanır.

Bu işlevleri BenchmarkDotNet gibi istatistiksel bir karşılaştırma aracıyla kıyasladığınızda, neredeyse %60 daha hızlı çalıştığını ve yönetilen yığında hiçbir şey ayırmadığını processStructPoint fark edeceksiniz.

Veri türü yüksek ayırma oranlarıyla küçük olduğunda yapısı ayrımcı birleşimleri göz önünde bulundurun

Yapı tanımlama demetleri ve kayıtlarıyla performansla ilgili önceki gözlemler F# Ayrımcı Birleşimler için de geçerlidir. Aşağıdaki kodu inceleyin:

    type Name = Name of string

    [<Struct>]
    type SName = SName of string

    let reverseName (Name s) =
        s.ToCharArray()
        |> Array.rev
        |> System.String
        |> Name

    let structReverseName (SName s) =
        s.ToCharArray()
        |> Array.rev
        |> System.String
        |> SName

Etki alanı modellemesi için bunun gibi tek durumlu Ayrımcı Birleşimler tanımlamak yaygın bir durumdır. Bu işlevleri BenchmarkDotNet gibi bir istatistiksel karşılaştırma aracıyla kıyasladığınızda, bunun küçük dizelere göre reverseName yaklaşık %25 daha hızlı çalıştığını göreceksinizstructReverseName. Büyük dizeler için her ikisi de yaklaşık olarak aynı işlemi gerçekleştirir. Bu durumda, bir yapı kullanmak her zaman tercih edilir. Daha önce belirtildiği gibi, her zaman ölçün ve varsayımlar veya sezgiler üzerinde çalışmayın.

Önceki örnekte Ayrımcı Birleşim yapısının daha iyi performans gösterdiği gösterilmiş olsa da, bir etki alanı modellendiğinde daha büyük Ayrımcı Birleşimler olması yaygın bir durumdur. Daha fazla kopyalama söz konusu olabileceğinden, bunun gibi daha büyük veri türleri, üzerindeki işlemlere bağlı olarak yapı olmaları durumunda da çalışmayabilir.

Değişmezlik ve mutasyon

F# değerleri varsayılan olarak sabittir ve bu da belirli hata sınıflarından (özellikle eşzamanlılık ve paralellik içeren sınıflardan) kaçınmanızı sağlar. Ancak, bazı durumlarda, yürütme süresi veya bellek ayırmalarında en uygun (veya hatta makul) verimliliği elde etmek için, bir çalışma aralığı en iyi şekilde yerinde durum mutasyonu kullanılarak uygulanabilir. Bu, anahtar sözcüğüyle F# ile mutable kabul etme temelinde mümkündür.

F# dilinde kullanımı mutable , işlevsel saflık ile anlaşmazlıklar içinde hissedebilir. Bu anlaşılabilir bir durumdur, ancak her yerde işlevsel saflık performans hedefleriyle çelişebilir. Uzlaşma, çağıranların işlev çağırdığında ne olacağıyla ilgilenmemesi için mutasyonu kapsüllemektir. Bu, performans açısından kritik kod için mutasyon tabanlı bir uygulama üzerinde işlevsel bir arabirim yazmanızı sağlar.

Ayrıca, F# let bağlama yapıları bağlamaları başka bir bağlamaya iç içe yerleştirmenize olanak tanır; bu, değişkenin mutable kapsamını yakın veya teorik en küçük düzeyde tutmak için kullanılabilir.

let data =
    [
        let mutable completed = false
        while not completed do
            logic ()
            // ...
            if someCondition then
                completed <- true   
    ]

Hiçbir kod, yalnızca let bağlı değerini başlatmak data için kullanılan değiştirilebilir completed değere erişemez.

Sabit arabirimlerde değiştirilebilir kodu sarmalama

Amaç olarak bilgi saydamlığıyla, performans açısından kritik işlevlerin değiştirilebilir altlığını kullanıma sunmayan kod yazmak kritik önem taşır. Örneğin, aşağıdaki kod işlevi F# çekirdek kitaplığında uygular Array.contains :

[<CompiledName("Contains")>]
let inline contains value (array:'T[]) =
    checkNonNull "array" array
    let mutable state = false
    let mutable i = 0
    while not state && i < array.Length do
        state <- value = array[i]
        i <- i + 1
    state

Bu işlevin birden çok kez çağrılması, temel diziyi değiştirmez ve bunu tüketen herhangi bir değiştirilebilir durumu korumanızı gerektirmez. İçinde hemen her kod satırı mutasyon kullansa da, referans olarak saydamdır.

Sınıflarda kapatılabilir verileri kapsülleyi göz önünde bulundurun

Önceki örnekte, değiştirilebilir verileri kullanarak işlemleri kapsüllemek için tek bir işlev kullanılmıştır. Bu, daha karmaşık veri kümeleri için her zaman yeterli değildir. Aşağıdaki işlev kümelerini göz önünde bulundurun:

open System.Collections.Generic

let addToClosureTable (key, value) (t: Dictionary<_,_>) =
    if t.ContainsKey(key) then
        t[key] <- value
    else
        t.Add(key, value)

let closureTableCount (t: Dictionary<_,_>) = t.Count

let closureTableContains (key, value) (t: Dictionary<_, HashSet<_>>) =
    match t.TryGetValue(key) with
    | (true, v) -> v.Equals(value)
    | (false, _) -> false

Bu kod yüksek performanslıdır, ancak çağıranların korumakla sorumlu olduğu mutasyona dayalı veri yapısını ortaya çıkarır. Bu, değiştirebilecek temel üyeleri olmayan bir sınıfın içinde sarmalanabilir:

open System.Collections.Generic

/// The results of computing the LALR(1) closure of an LR(0) kernel
type Closure1Table() =
    let t = Dictionary<Item0, HashSet<TerminalIndex>>()

    member _.Add(key, value) =
        if t.ContainsKey(key) then
            t[key] <- value
        else
            t.Add(key, value)

    member _.Count = t.Count

    member _.Contains(key, value) =
        match t.TryGetValue(key) with
        | (true, v) -> v.Equals(value)
        | (false, _) -> false

Closure1Table temel alınan mutasyona dayalı veri yapısını kapsüller, böylece çağıranları temel alınan veri yapısını korumaya zorlamaz. Sınıflar, ayrıntıları arayanlara göstermeden mutasyon tabanlı verileri ve rutinleri kapsüllemenin güçlü bir yoludur.

Tercih let mutable etme ref

Başvuru hücreleri, değerin kendisi yerine bir değere yapılan başvuruyu temsil etmenin bir yoludur. Performans açısından kritik kod için kullanılabilseler de, bunlar önerilmez. Aşağıdaki örneği inceleyin:

let kernels =
    let acc = ref Set.empty

    processWorkList startKernels (fun kernel ->
        if not ((!acc).Contains(kernel)) then
            acc := (!acc).Add(kernel)
        ...)

    !acc |> Seq.toList

Bir başvuru hücresinin kullanılması artık temel alınan verilere başvurmak ve yeniden başvurmak zorunda kalmadan sonraki tüm kodları "kirletir". Bunun yerine, göz önünde bulundurun let mutable:

let kernels =
    let mutable acc = Set.empty

    processWorkList startKernels (fun kernel ->
        if not (acc.Contains(kernel)) then
            acc <- acc.Add(kernel)
        ...)

    acc |> Seq.toList

Lambda ifadesinin ortasındaki tek mutasyon noktasının yanı sıra, dokunan diğer tüm kodlar acc bunu normal letbağlı sabit değerin kullanımından farklı olmayan bir şekilde yapabilir. Bu, zaman içinde değişmeyi kolaylaştırır.

Null değerler ve varsayılan değerler

F# dilinde null değerlerden genellikle kaçınılmalıdır. Varsayılan olarak F#tarafından bildirilen türler değişmez değerin null kullanımını desteklemez ve tüm değerler ve nesneler başlatılır. Ancak, bazı yaygın .NET API'leri null değer döndürür veya kabul eder ve bazıları da ortaktır. Diziler ve dizeler gibi NET tarafından bildirilen türler null değerlere izin verir. Ancak, F# programlamada değerlerin null ortaya çıkması çok nadirdir ve F# kullanmanın avantajlarından biri çoğu durumda null başvuru hatalarından kaçınmaktır.

özniteliğini kullanmaktan AllowNullLiteral kaçının

Varsayılan olarak F#-bildirilen türler değişmez değerin null kullanımını desteklemez. Buna izin vermek için F# türlerine el ile AllowNullLiteral açıklama ekleyebilirsiniz. Ancak, bunu yapmaktan kaçınmak neredeyse her zaman daha iyidir.

özniteliğini kullanmaktan Unchecked.defaultof<_> kaçının

kullanarak Unchecked.defaultof<_>bir null F# türü için veya sıfır başlatılan bir değer oluşturmak mümkündür. Bu, bazı veri yapıları için depolamayı başlatırken veya yüksek performanslı bir kodlama düzeninde veya birlikte çalışabilirlik içinde yararlı olabilir. Ancak bu yapıyı kullanmaktan kaçınılmalıdır.

özniteliğini kullanmaktan DefaultValue kaçının

Varsayılan olarak, F# kayıtları ve nesneleri oluşturma sırasında düzgün şekilde başlatılmalıdır. DefaultValue özniteliği, bazı nesne alanlarını veya sıfır başlatılan bir null değerle doldurmak için kullanılabilir. Bu yapıya nadiren ihtiyaç duyulmaktadır ve kullanımından kaçınılmalıdır.

Null girişleri denetlerseniz, ilk fırsatta özel durumlar tetikler

Yeni F# kodu yazarken, kodun C# veya diğer .NET dillerinden kullanılmasını beklemediğiniz sürece uygulamada null girişleri denetlemeniz gerekmez.

Null girişler için denetimler eklemeye karar verirseniz, ilk fırsatta denetimleri gerçekleştirin ve bir özel durum tetikler. Örneğin:

let inline checkNonNull argName arg =
    if isNull arg then
        nullArg argName

module Array =
    let contains value (array:'T[]) =
        checkNonNull "array" array
        let mutable result = false
        let mutable i = 0
        while not state && i < array.Length do
            result <- value = array[i]
            i <- i + 1
        result

Eski nedenlerden dolayı, FSharp.Core'daki bazı dize işlevleri null değerleri boş dizeler olarak kabul eder ve null bağımsız değişkenlerde başarısız olmaz. Ancak bunu kılavuz olarak almayın ve herhangi bir anlamsal anlamı "null" ile ilişkilendiren kodlama desenlerini benimsemeyin.

Nesne programlama

F# nesneleri ve nesne odaklı (OO) kavramları için tam desteğe sahiptir. Birçok OO kavramı güçlü ve kullanışlı olsa da, bunların tümü kullanmak için ideal değildir. Aşağıdaki listelerde, yüksek düzeyde OO özellikleri kategorileri hakkında rehberlik sunulmaktadır.

Bu özellikleri birçok durumda kullanmayı göz önünde bulundurun:

  • Noktalı gösterimi (x.Length)
  • Örnek üyeleri
  • Örtük oluşturucular
  • Statik üyeler
  • Dizin oluşturucu gösterimi (arr[x]), bir Item özellik tanımlayarak
  • Üyeleri tanımlayarak GetSlice dilimleme gösterimi (arr[x..y], arr[x..], arr[..y]),
  • Adlandırılmış ve İsteğe Bağlı bağımsız değişkenler
  • Arabirimler ve arabirim uygulamaları

Önce bu özelliklere erişmeyin, ancak bir sorunu çözmek için uygun olduklarında bunları mantıklı bir şekilde uygulayın:

  • Yöntem aşırı yükleme
  • Kapsüllenmiş değiştirilebilir veriler
  • Türlerdeki işleçler
  • Otomatik özellikler
  • uygulama IDisposable ve IEnumerable
  • Tür uzantıları
  • Ekinlikler
  • Yapılar
  • Temsilciler
  • Numaralandırmalar

Bu özellikleri kullanmanız gerekmediği sürece genellikle bu özelliklerden kaçının:

  • Devralma tabanlı tür hiyerarşileri ve uygulama devralma
  • Null'lar ve Unchecked.defaultof<_>

Birleştirmeyi devralma yerine tercih et

Devralma yerine oluşturma, iyi F# kodunun bağlı kalabileceği uzun süreli bir deyimdir. Temel ilke, bir temel sınıfı kullanıma sunmamalı ve işlev elde etmek için çağıranları bu temel sınıftan devralmaya zorlamamalısınız.

Sınıfa ihtiyacınız yoksa arabirimleri uygulamak için nesne ifadelerini kullanma

Nesne İfadeleri , bir sınıfın içinde bunu yapmanıza gerek kalmadan uygulanan arabirimi bir değere bağlayarak arabirimleri anında uygulamanıza olanak sağlar. Bu, özellikle yalnızca arabirimi uygulamanız gerekiyorsa ve tam bir sınıfa ihtiyacınız yoksa kullanışlıdır.

Örneğin, deyimine sahip open olmadığınız bir simge eklediyseniz kod düzeltme eylemi sağlamak için Ionide'da çalıştırılacak kod aşağıda verilmiştir:

    let private createProvider () =
        { new CodeActionProvider with
            member this.provideCodeActions(doc, range, context, ct) =
                let diagnostics = context.diagnostics
                let diagnostic = diagnostics |> Seq.tryFind (fun d -> d.message.Contains "Unused open statement")
                let res =
                    match diagnostic with
                    | None -> [||]
                    | Some d ->
                        let line = doc.lineAt d.range.start.line
                        let cmd = createEmpty<Command>
                        cmd.title <- "Remove unused open"
                        cmd.command <- "fsharp.unusedOpenFix"
                        cmd.arguments <- Some ([| doc |> unbox; line.range |> unbox; |] |> ResizeArray)
                        [|cmd |]
                res
                |> ResizeArray
                |> U2.Case1
        }

Visual Studio Code API'siyle etkileşim kurarken sınıfa gerek olmadığından, Nesne İfadeleri bunun için ideal bir araçtır. Ayrıca, test yordamlarına sahip bir arabirimi doğaçlama bir şekilde saptamak istediğinizde birim testi için de değerlidir.

İmzaları kısaltmak için Tür Kısaltmalarını göz önünde bulundurun

Tür Kısaltmaları , işlev imzası veya daha karmaşık bir tür gibi başka bir türe etiket atamanın kullanışlı bir yoludur. Örneğin, aşağıdaki diğer ad bir derin öğrenme kitaplığı olan CNTK ile hesaplama tanımlamak için gerekenlere bir etiket atar:

open CNTK

// DeviceDescriptor, Variable, and Function all come from CNTK
type Computation = DeviceDescriptor -> Variable -> Function

Ad Computation , diğer adla eşleştiğinden herhangi bir işlevi belirtmek için kullanışlı bir yoldur. Bunun gibi Tür Kısaltmaları kullanmak kullanışlıdır ve daha kısa kod sağlar.

Etki alanınızı temsil etmek için Tür Kısaltmaları kullanmaktan kaçının

Tür Kısaltmaları işlev imzalarına ad vermek için kullanışlı olsa da, diğer türleri kısaltırken kafa karıştırıcı olabilir. Şu kısaltmayı göz önünde bulundurun:

// Does not actually abstract integers.
type BufferSize = int

Bu, çeşitli yollarla kafa karıştırıcı olabilir:

  • BufferSize soyutlama değildir; bir tamsayı için yalnızca başka bir addır.
  • Genel API'de kullanıma sunulursa BufferSize , yalnızca int'den daha fazlasını ifade etmek için kolayca yanlış yorumlanabilir. Genel olarak, etki alanı türlerinin bunlara birden çok özniteliği vardır ve gibi intilkel türler değildir. Bu kısaltma bu varsayımı ihlal eder.
  • (PascalCase) büyük/küçük BufferSize harfleri, bu türün daha fazla veri barındırdığını gösterir.
  • Bu diğer ad, işleve adlandırılmış bağımsız değişken sağlamaya kıyasla daha fazla netlik sunmaz.
  • Kısaltma derlenmiş IL'de gösterilmez; yalnızca bir tamsayıdır ve bu diğer ad bir derleme zamanı yapısıdır.
module Networking =
    ...
    let send data (bufferSize: int) = ...

Özetle, Tür Kısaltmaları ile ilgili tuzak, kısaltma yaptıkları türlerin soyutlamaları olmamasıdır . Önceki örnekte, BufferSize yalnızca kapakların altında, ek veri içermeyen bir int örnektir ve zaten sahip olanın int yanı sıra tür sisteminden herhangi bir fayda sağlayamaz.

Etki alanını temsil etmek için tür kısaltmaları kullanmanın alternatif bir yaklaşımı, tek büyük/küçük harf ayrımcı birleşimleri kullanmaktır. Önceki örnek aşağıdaki gibi modellenebilir:

type BufferSize = BufferSize of int

ve temel değeri açısından BufferSize çalışan bir kod yazarsanız, rastgele bir tamsayı geçirmek yerine bir kod oluşturmanız gerekir:

module Networking =
    ...
    let send data (BufferSize size) =
    ...

Çağıranın işlevi çağırmadan önce bir değeri sarmalayan bir BufferSize tür oluşturması gerektiğinden, bu işleve yanlışlıkla rastgele bir tamsayı send geçirme olasılığını azaltır.