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 static
kullanı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 dep2
ile 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 dep3
ihlal 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:
- Bağımlı durumları API'nin dışına gönderme.
- Yapılandırma artık API'nin dışında yapılabilir.
- Bağımlı değerler için başlatma hatalarının olarak
TypeInitializationException
bildirilme olasılığı yüksek değildir. - 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:
- Etki alanınız zaman içinde değiştikçe bakımını yapmak daha kolaydır.
- 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. |
, invalidArg
ve invalidOp
uygun olduğunda oluşturma ArgumentNullException
mekanizması olarak , ArgumentException
ve 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. let
F# 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
private
korumak 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 inline
iş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 let
bağ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]
), birItem
ö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
veIEnumerable
- 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ızcaint
'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 gibiint
ilkel 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.