Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Dokumen ini adalah serangkaian pedoman desain komponen untuk pemrograman F#, berdasarkan Panduan Desain Komponen F#, v14, Microsoft Research, dan versi yang awalnya dikumpulkan dan dikelola oleh F# Software Foundation.
Dokumen ini mengasumsikan Anda terbiasa dengan pemrograman F#. Banyak yang berterima kasih kepada komunitas F# atas kontribusi mereka dan umpan balik yang bermanfaat pada berbagai versi panduan ini.
Ikhtisar
Dokumen ini melihat beberapa masalah yang terkait dengan desain dan pengkodan komponen F#. Komponen dapat berarti salah satu hal berikut:
- Lapisan dalam proyek F# Anda yang memiliki konsumen eksternal dalam proyek tersebut.
- Pustaka yang ditujukan untuk dikonsumsi oleh kode F# di seluruh batas perakitan.
- Pustaka yang ditujukan untuk digunakan oleh bahasa .NET apa pun di seluruh batas antarmuka.
- Pustaka yang ditujukan untuk distribusi melalui repositori paket, seperti NuGet.
Teknik yang dijelaskan dalam artikel ini mengikuti Lima prinsip kode F# yang baik, dan dengan demikian menggunakan pemrograman fungsi dan objek sebagaimana mestinya.
Terlepas dari metodologinya, komponen dan perancang pustaka menghadapi sejumlah masalah praktis dan prosaik saat mencoba membuat API yang paling mudah digunakan oleh pengembang. Penerapan yang hati-hati dari Pedoman Desain Pustaka .NET akan mengarahkan Anda untuk membuat sekumpulan API yang konsisten yang menyenangkan untuk digunakan.
Pedoman umum
Ada beberapa pedoman universal yang berlaku untuk perpustakaan F#, tanpa memandang audiens yang dimaksudkan untuk perpustakaan tersebut.
Pelajari Panduan Desain Pustaka .NET
Terlepas dari jenis pengkodian F# yang Anda lakukan, sangat berharga untuk memiliki pengetahuan kerja tentang Pedoman Desain Pustaka .NET . Sebagian besar programmer F# dan .NET lainnya akan terbiasa dengan pedoman ini, dan mengharapkan kode .NET sesuai dengan mereka.
Pedoman Desain Pustaka .NET memberikan panduan umum mengenai penamaan, merancang kelas dan antarmuka, desain anggota (properti, metode, peristiwa, dll.) dan banyak lagi, dan merupakan titik referensi pertama yang berguna untuk berbagai panduan desain.
Menambahkan komentar dokumentasi XML ke kode Anda
Dokumentasi XML pada API publik memastikan bahwa pengguna dapat merasakan Intellisense dan Quickinfo yang hebat saat menggunakan tipe dan anggota ini, serta memungkinkan pembuatan berkas dokumentasi untuk pustaka. Lihat Dokumentasi XML tentang berbagai tag XML yang dapat digunakan untuk penandaan tambahan dalam komentar xmldoc.
/// A class for representing (x,y) coordinates
type Point =
/// Computes the distance between this point and another
member DistanceTo: otherPoint:Point -> float
Anda dapat menggunakan komentar XML formulir pendek (/// comment
), atau komentar XML standar (///<summary>comment</summary>
).
Pertimbangkan untuk menggunakan file tanda tangan eksplisit (.fsi) untuk API pustaka dan komponen yang stabil
Menggunakan file tanda tangan eksplisit di pustaka F# menyediakan ringkasan API publik yang singkat, yang membantu memastikan bahwa Anda mengetahui permukaan publik lengkap pustaka Anda, dan menyediakan pemisahan yang bersih antara dokumentasi publik dan detail implementasi internal. Berkas tanda tangan menambah hambatan pada perubahan API publik, dengan mengharuskan perubahan dilakukan baik pada berkas implementasi maupun berkas tanda tangan. Akibatnya, file tanda tangan biasanya hanya boleh diperkenalkan ketika API telah stabil dan diharapkan tidak akan mengalami perubahan signifikan.
Ikuti praktik terbaik untuk menggunakan string di .NET
Ikuti panduan Praktik Terbaik untuk Menggunakan String dalam .NET ketika sesuai dengan cakupan proyek. Secara khusus, menyatakan maksud budaya secara eksplisit dalam konversi dan membandingkan string (jika berlaku).
Panduan untuk pustaka yang digunakan dengan F#
Bagian ini menyajikan rekomendasi untuk mengembangkan pustaka publik yang menghadap F#; artinya, pustaka yang mengekspos API publik yang ditujukan untuk digunakan oleh pengembang F#. Ada berbagai rekomendasi desain pustaka yang berlaku khusus untuk F#. Dalam ketidakhadiran rekomendasi spesifik yang mengikuti, Panduan Desain Pustaka .NET adalah panduan cadangan.
Konvensi penamaan
Menggunakan konvensi penamaan dan kapitalisasi .NET
Tabel berikut mengikuti konvensi penamaan dan kapitalisasi .NET. Terdapat tambahan kecil untuk memasukkan juga konstruksi F#. Rekomendasi tersebut terutama dimaksudkan untuk API yang melampaui batas F#-ke-F#, cocok dengan idiom dari .NET BCL dan sebagian besar pustaka.
Membangun | Perkara | Bagian | Contoh | Catatan |
---|---|---|---|---|
Jenis beton | PascalCase | Kata benda/kata sifat | Daftar, Ganda, Kompleks | Tipe konkret adalah struct, kelas, enumerasi, delegasi, rekaman, dan union. Meskipun nama tipe secara tradisional ditulis dengan huruf kecil di OCaml, F# telah mengadopsi skema penamaan .NET untuk tipe. |
DLL | PascalCase | Fabrikam.Core.dll | ||
Tag Serikat | PascalCase | Kata benda | Beberapa, Tambahkan, Berhasil | Jangan gunakan awalan di API publik. Secara opsional gunakan awalan saat internal, seperti "ketik Teams = TAlpha | TBeta | TDelta". |
Peristiwa | PascalCase | Kata kerja | PerubahanNilai / NilaiSedangBerubah | |
Pengecualian | PascalCase | WebException | Nama harus diakhir dengan "Pengecualian". | |
Kebun | PascalCase | Kata benda | NamaSaatIni | |
Jenis antarmuka | PascalCase | Kata benda/kata sifat | IDisposable | Nama harus dimulai dengan "I". |
Metode | PascalCase | Kata kerja | Mengubah menjadi string | |
Ruang Nama | PascalCase | Microsoft.FSharp.Core | Umumnya gunakan <Organization>.<Technology>[.<Subnamespace>] , tetapi hilangkan nama organisasi jika teknologi tidak bergantung pada organisasi. |
|
Parameter | camelCase | Kata benda | namaJenis, transformasi, rentang | |
biarkan nilai internal | camelCase atau PascalCase | Kata benda/kata kerja | getValue, myTable | |
biarkan nilai (eksternal) | camelCase atau PascalCase | Kata benda/kata kerja | List.map, Tanggal.HariIni | nilai let-bound sering kali bersifat publik ketika mengikuti pola desain fungsi tradisional. Namun, umumnya menggunakan PascalCase ketika pengidentifikasi dapat digunakan dari bahasa .NET lainnya. |
Harta benda | PascalCase | Kata benda/kata sifat | IsEndOfFile, BackColor | Properti Boolean umumnya menggunakan Is dan Can dan harus afirmatif, seperti dalam IsEndOfFile, bukan IsNotEndOfFile. |
Hindari singkatan
Pedoman .NET mencegah penggunaan singkatan (misalnya, "gunakan OnButtonClick
daripada OnBtnClick
"). Singkatan umum, seperti Async
untuk "Asinkron", ditoleransi. Pedoman ini terkadang diabaikan untuk pemrograman fungsi; misalnya, List.iter
menggunakan singkatan untuk "iterate". Untuk alasan ini, menggunakan singkatan cenderung ditoleransi ke tingkat yang lebih besar dalam pemrograman F#-ke-F#, tetapi umumnya masih harus dihindari dalam desain komponen publik.
Hindari tabrakan nama casing
Panduan .NET mengatakan bahwa casing saja tidak dapat digunakan untuk membedakan konflik nama, karena beberapa bahasa klien (misalnya, Visual Basic) tidak peka terhadap perbedaan huruf besar/kecil.
Gunakan akronim jika sesuai
Akronim seperti XML bukan singkatan dan banyak digunakan dalam pustaka .NET dalam bentuk yang tidak dikapitalisasi (Xml). Hanya akronim yang dikenal luas yang harus digunakan.
Menggunakan PascalCase untuk nama parameter generik
Gunakan PascalCase untuk nama parameter generik di API publik, termasuk untuk pustaka yang digunakan dengan F#. Secara khusus, gunakan nama seperti T
, U
, T1
, T2
untuk parameter generik arbitrer, dan ketika nama tertentu masuk akal, maka untuk pustaka yang menghadap F#menggunakan nama seperti Key
, Value
, Arg
(tetapi tidak misalnya, TKey
).
Gunakan PascalCase atau camelCase untuk fungsi dan nilai publik dalam modul F#
camelCase digunakan untuk fungsi publik yang dirancang untuk digunakan tanpa kualifikasi (misalnya, invalidArg
), dan untuk "fungsi koleksi standar" (misalnya, List.map). Dalam kedua kasus ini, nama fungsi bertindak seperti kata kunci dalam bahasa.
Desain Objek, Jenis, dan Modul
Gunakan namespace atau modul untuk mengelompokkan jenis dan modul Anda
Setiap file F# dalam komponen harus dimulai dengan deklarasi namespace layanan atau deklarasi modul.
namespace Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
atau
module Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
Perbedaan antara menggunakan modul dan namespace untuk mengatur kode di tingkat atas adalah sebagai berikut:
- Namespace dapat menjangkau beberapa file
- Namespace tidak boleh berisi fungsi F# kecuali berada dalam modul dalam
- Kode untuk modul tertentu harus dimuat dalam satu file
- Modul tingkat atas dapat berisi fungsi F# tanpa perlu modul dalam
Pilihan antara namespace atau modul tingkat atas memengaruhi bentuk kode yang dikompilasi, dan dengan demikian akan memengaruhi tampilan dari bahasa .NET lainnya jika API Anda akhirnya digunakan di luar kode F#.
Menggunakan metode dan properti untuk operasi yang melekat pada jenis objek
Saat bekerja dengan objek, yang terbaik adalah memastikan bahwa fungsionalitas yang dapat dikonsumsi diimplementasikan sebagai metode dan properti pada jenis tersebut.
type HardwareDevice() =
member this.ID = ...
member this.SupportedProtocols = ...
type HashTable<'Key,'Value>(comparer: IEqualityComparer<'Key>) =
member this.Add(key, value) = ...
member this.ContainsKey(key) = ...
member this.ContainsValue(value) = ...
Sebagian besar fungsionalitas untuk anggota tertentu tidak perlu diimplementasikan dalam anggota tersebut, tetapi bagian yang dapat dikonsumsi dari fungsionalitas tersebut harus.
Menggunakan kelas untuk merangkum status yang dapat diubah
Dalam F#, ini hanya perlu dilakukan di mana status tersebut belum dienkapsulasi oleh konstruksi bahasa lain, seperti penutupan, ekspresi urutan, atau komputasi asinkron.
type Counter() =
// let-bound values are private in classes.
let mutable count = 0
member this.Next() =
count <- count + 1
count
Menggunakan antarmuka untuk mengelompokkan operasi terkait
Gunakan jenis antarmuka untuk mewakili serangkaian operasi. Ini lebih disukai daripada opsi lain, seperti tuple fungsi atau rekaman fungsi.
type Serializer =
abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T
Sebagai pilihan dibandingkan:
type Serializer<'T> = {
Serialize: bool -> 'T -> string
Deserialize: bool -> string -> 'T
}
Antarmuka adalah konsep kelas satu di .NET, yang dapat Anda gunakan untuk mencapai apa yang biasanya diberikan Functors. Selain itu, mereka dapat digunakan untuk mengodekan jenis eksistensial ke dalam program Anda, yang tidak dapat dilakukan oleh rekaman fungsi.
Menggunakan modul untuk mengelompokkan fungsi yang bertindak berdasarkan koleksi
Saat Anda menentukan jenis koleksi, pertimbangkan untuk menyediakan serangkaian operasi standar seperti CollectionType.map
dan CollectionType.iter
) untuk jenis koleksi baru.
module CollectionType =
let map f c =
...
let iter f c =
...
Jika Anda menyertakan modul seperti itu, ikuti konvensi penamaan standar untuk fungsi yang ditemukan di FSharp.Core.
Gunakan modul untuk mengelompokkan fungsi untuk fungsi kanonis umum, terutama dalam pustaka matematika dan DSL
Misalnya, Microsoft.FSharp.Core.Operators
adalah kumpulan fungsi tingkat atas yang dibuka secara otomatis (seperti abs
dan sin
) yang disediakan oleh FSharp.Core.dll.
Demikian juga, pustaka statistik mungkin menyertakan modul dengan fungsi erf
dan erfc
, di mana modul ini dirancang untuk dibuka secara eksplisit atau otomatis.
Pertimbangkan untuk menggunakan RequireQualifiedAccess dan terapkan atribut AutoOpen dengan hati-hati
Menambahkan atribut [<RequireQualifiedAccess>]
ke modul menunjukkan bahwa modul mungkin tidak dibuka dan yang mereferensikan ke elemen modul memerlukan akses eksplisit yang memenuhi syarat. Misalnya, modul Microsoft.FSharp.Collections.List
memiliki atribut ini.
Ini berguna ketika fungsi dan nilai dalam modul memiliki nama yang kemungkinan bertentangan dengan nama di modul lain. Membutuhkan akses yang memenuhi syarat dapat sangat meningkatkan ketahanan dan perkembangan pustaka jangka panjang.
Sangat disarankan untuk memiliki atribut [<RequireQualifiedAccess>]
untuk modul kustom yang memperluas yang disediakan oleh FSharp.Core
(seperti Seq
, List
, Array
), karena modul tersebut sebelumnya digunakan dalam kode F# dan telah [<RequireQualifiedAccess>]
ditentukan pada modul tersebut; lebih umum, tidak disarankan untuk menentukan modul kustom yang tidak memiliki atribut, ketika modul tersebut membayangi atau memperluas modul lain yang memiliki atribut .
Menambahkan atribut [<AutoOpen>]
ke modul berarti modul akan dibuka saat namespace yang berisi dibuka. Atribut [<AutoOpen>]
juga dapat diterapkan ke rakitan untuk menunjukkan modul yang dibuka secara otomatis saat rakitan direferensikan.
Misalnya, pustaka statistik MathsHeaven.Statistics mungkin berisi module MathsHeaven.Statistics.Operators
yang berisi fungsi erf
dan erfc
. Masuk akal untuk menandai modul ini sebagai [<AutoOpen>]
. Ini berarti open MathsHeaven.Statistics
juga akan membuka modul ini dan membawa nama erf
dan erfc
ke dalam cakupan. Penggunaan lain yang baik dari [<AutoOpen>]
adalah untuk modul yang berisi metode ekstensi.
Penggunaan [<AutoOpen>]
secara berlebihan menyebabkan namespace tercemar, dan atribut harus digunakan dengan hati-hati. Untuk pustaka tertentu di domain tertentu, penggunaan [<AutoOpen>]
yang bijaksana dapat meningkatkan kegunaan.
Pertimbangkan untuk mendefinisikan anggota operator pada kelas jika penggunaan operator yang sudah dikenal itu tepat.
Terkadang kelas digunakan untuk memodelkan konstruksi matematika seperti Vektor. Ketika domain yang dimodelkan memiliki operator terkenal, menentukannya sebagai anggota intrinsik ke kelas sangat membantu.
type Vector(x: float) =
member v.X = x
static member (*) (vector: Vector, scalar: float) = Vector(vector.X * scalar)
static member (+) (vector1: Vector, vector2: Vector) = Vector(vector1.X + vector2.X)
let v = Vector(5.0)
let u = v * 10.0
Panduan ini sesuai dengan panduan .NET umum untuk jenis ini. Namun, juga penting dalam pengodean F# karena ini memungkinkan jenis ini digunakan bersama dengan fungsi dan metode F# dengan batasan anggota, seperti List.sumBy.
Pertimbangkan untuk menggunakan CompiledName untuk menyediakan nama yang ramah .NET bagi pengguna bahasa .NET lainnya.
Terkadang Anda mungkin ingin menamai sesuatu dengan cara tertentu untuk pengguna F# (seperti anggota statis yang ditulis dengan huruf kecil sehingga terlihat seperti fungsi yang terikat dengan modul), namun menggunakan gaya yang berbeda pada nama tersebut saat dikompilasi menjadi sebuah rakitan. Anda dapat menggunakan atribut [<CompiledName>]
untuk memberikan gaya yang berbeda untuk kode non F# yang menggunakan assembly.
type Vector(x:float, y:float) =
member v.X = x
member v.Y = y
[<CompiledName("Create")>]
static member create x y = Vector (x, y)
let v = Vector.create 5.0 3.0
Dengan menggunakan [<CompiledName>]
, Anda dapat menggunakan konvensi penamaan .NET untuk pengguna assembly yang tidak menggunakan F#.
Gunakan metode kelebihan beban untuk fungsi anggota, jika melakukannya menyediakan API yang lebih sederhana
Metode kelebihan beban adalah alat yang ampuh untuk menyederhanakan API yang mungkin perlu melakukan fungsionalitas serupa, tetapi dengan opsi atau argumen yang berbeda.
type Logger() =
member this.Log(message) =
...
member this.Log(message, retryPolicy) =
...
Dalam F#, lebih umum melakukan overloading berdasarkan jumlah argumen daripada jenis argumen.
Sembunyikan representasi tipe rekaman dan tipe gabungan jika desain tipe ini kemungkinan akan berkembang
Hindari mengungkapkan representasi konkret objek. Misalnya, representasi konkret dari nilai DateTime tidak diungkapkan oleh API publik eksternal dari desain pustaka .NET. Pada saat waktu berjalan, Common Language Runtime mengetahui implementasi yang akan digunakan sepanjang eksekusi. Namun, kode yang dikompilasi tidak secara langsung mengambil dependensi pada representasi konkret.
Hindari penggunaan pewarisan implementasi untuk ekstensibilitas
Dalam F#, pewarisan implementasi jarang digunakan. Selain itu, hierarki warisan seringkali rumit dan sulit diubah ketika persyaratan baru tiba. Implementasi warisan masih ada di F# untuk kompatibilitas dan kasus langka di mana itu adalah solusi terbaik untuk masalah, tetapi teknik alternatif harus dicari dalam program F# Anda saat merancang polimorfisme, seperti implementasi antarmuka.
Tanda tangan fungsi dan anggota
Menggunakan tuple untuk mengembalikan nilai saat mengembalikan sejumlah kecil beberapa nilai yang tidak terkait
Berikut adalah contoh yang baik penggunaan tuple untuk jenis pengembalian:
val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger
Untuk tipe pengembalian yang berisi banyak komponen, atau di mana komponen terkait dengan satu entitas yang dapat diidentifikasi, pertimbangkan untuk menggunakan tipe bernama daripada tuple.
Gunakan Async<T>
untuk pemrograman asinkron di batas F# API
Jika ada operasi sinkron terkait bernama Operation
yang mengembalikan T
, maka operasi asinkron harus diberi nama AsyncOperation
jika mengembalikan Async<T>
atau OperationAsync
jika mengembalikan Task<T>
. Untuk jenis .NET yang umum digunakan yang mengekspos metode Begin/End, pertimbangkan untuk menggunakan Async.FromBeginEnd
untuk menulis metode ekstensi sebagai fasad untuk menyediakan model pemrograman asinkron F# ke API .NET tersebut.
type SomeType =
member this.Compute(x:int): int =
...
member this.AsyncCompute(x:int): Async<int> =
...
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
...
Pengecualian
Lihat Manajemen Kesalahan untuk mempelajari tentang penggunaan pengecualian, hasil, dan opsi yang sesuai.
Anggota Ekstensi
Terapkan anggota ekstensi F# secara hati-hati dalam komponen F# ke F#
Anggota ekstensi F# umumnya hanya boleh digunakan untuk operasi yang berada dalam cakupan operasi intrinsik yang terkait dengan jenis dalam sebagian besar cara penggunaannya. Salah satu penggunaan umum adalah menyediakan API yang lebih idiomatik untuk F# untuk berbagai jenis .NET:
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
Async.FromBeginEnd(this.BeginReceive, this.EndReceive)
type System.Collections.Generic.IDictionary<'Key,'Value> with
member this.TryGet key =
let ok, v = this.TryGetValue key
if ok then Some v else None
Jenis Union
Gunakan serikat pekerja yang didiskriminasi alih-alih hierarki kelas untuk data terstruktur pohon
Struktur seperti pohon didefinisikan secara rekursif. Ini canggung dengan pewarisan, tetapi elegan dengan Union Diskriminatif.
type BST<'T> =
| Empty
| Node of 'T * BST<'T> * BST<'T>
Mewakili data seperti pohon dengan Discriminated Unions juga memungkinkan Anda untuk mendapatkan manfaat dari kelengkapan dalam pencocokan pola.
Gunakan [<RequireQualifiedAccess>]
pada tipe gabungan yang nama pembedanya tidak cukup unik
Anda mungkin menemukan diri Anda di bidang di mana nama yang sama adalah yang terbaik untuk berbagai hal, seperti kasus Discriminated Union. Anda dapat menggunakan [<RequireQualifiedAccess>]
untuk membedakan nama kasus untuk menghindari memicu kesalahan yang membingungkan karena bayangan tergantung pada urutan pernyataan open
Sembunyikan representasi union terdiferensiasi untuk API yang kompatibel secara biner jika desain jenis ini kemungkinan besar akan berkembang
Jenis union mengandalkan pola pencocokan F# untuk model pemrograman yang ringkas. Seperti disebutkan sebelumnya, Anda harus menghindari mengungkapkan representasi data konkret jika desain jenis ini cenderung berkembang.
Misalnya, representasi serikat yang didiskriminasi dapat disembunyikan menggunakan deklarasi privat atau internal, atau dengan menggunakan file tanda tangan.
type Union =
private
| CaseA of int
| CaseB of string
Jika Anda mengungkapkan serikat yang didiskriminasi secara tidak jelas, Anda mungkin merasa sulit untuk membuat versi pustaka Anda tanpa melanggar kode pengguna. Sebagai gantinya, pertimbangkan untuk memperlihatkan satu atau beberapa pola aktif untuk mengizinkan pencocokan pola atas nilai tipe Anda.
Pola Aktif menyediakan cara alternatif untuk memberi pengguna F# pencocokan pola sambil menghindari mengekspos Jenis Union F# secara langsung.
Fungsi Inline dan Batasan Anggota
Menentukan algoritma numerik generik menggunakan fungsi sebaris dengan batasan anggota tersirat dan jenis generik yang diselesaikan secara statis
Batasan anggota aritmatika dan batasan perbandingan F# adalah standar untuk pemrograman F#. Misalnya, pertimbangkan kode berikut:
let inline highestCommonFactor a b =
let rec loop a b =
if a = LanguagePrimitives.GenericZero<_> then b
elif a < b then loop a (b - a)
else loop (a - b) b
loop a b
Jenis fungsi ini adalah sebagai berikut:
val inline highestCommonFactor : ^T -> ^T -> ^T
when ^T : (static member Zero : ^T)
and ^T : (static member ( - ) : ^T * ^T -> ^T)
and ^T : equality
and ^T : comparison
Ini adalah fungsi yang cocok untuk API publik di pustaka matematika.
Hindarilah menggunakan batasan anggota untuk mensimulasikan type classes dan duck typing.
Dimungkinkan untuk mensimulasikan "pengetikan bebek" menggunakan batasan anggota F#. Namun, anggota yang menggunakan ini sebaiknya tidak digunakan dalam desain pustaka F#-ke-F#. Ini karena desain pustaka berdasarkan batasan implisit yang tidak dikenal atau tidak standar cenderung menyebabkan kode pengguna menjadi tidak fleksibel dan terikat pada satu pola kerangka kerja tertentu.
Selain itu, ada kemungkinan besar bahwa penggunaan batasan anggota yang berat dengan cara ini dapat mengakibatkan waktu kompilasi yang sangat lama.
Definisi Operator
Hindari menentukan operator simbolis kustom
Operator kustom sangat penting dalam beberapa situasi dan merupakan perangkat notasi yang sangat berguna dalam isi kode implementasi yang besar. Untuk pengguna baru pustaka, fungsi bernama sering kali lebih mudah digunakan. Selain itu, operator simbolis kustom bisa sulit didokumenkan, dan pengguna merasa lebih sulit untuk mencari bantuan pada operator, karena keterbatasan yang ada di IDE dan mesin pencari.
Akibatnya, yang terbaik adalah memublikasikan fungsionalitas Anda sebagai fungsi dan anggota yang bernama, dan juga mengungkapkan operator untuk fungsionalitas ini hanya jika manfaat notasi melebihi dibandingkan dengan dokumentasi dan biaya kognitif untuk memilikinya.
Satuan Ukuran
Gunakan unit pengukuran dengan hati-hati untuk keamanan jenis tambahan dalam kode F#
Informasi pengetikan tambahan untuk unit pengukuran dihapus saat dilihat oleh bahasa .NET lainnya. Harap diperhatikan bahwa komponen, perangkat, dan refleksi .NET akan melihat tipe tanpa unit. Misalnya, konsumen C# akan melihat float
daripada float<kg>
.
Singkatan Jenis
Gunakan singkatan jenis dengan hati-hati untuk menyederhanakan kode F#
Komponen, alat, dan refleksi .NET tidak akan memiliki nama singkatan untuk tipe. Penggunaan singkatan jenis yang signifikan juga dapat membuat domain tampak lebih kompleks daripada sebenarnya, yang dapat membingungkan konsumen.
Hindari singkatan jenis untuk jenis publik yang anggota dan propertinya harus secara intrinsik berbeda dengan yang tersedia pada jenis yang disingkat
Dalam hal ini, tipe yang disingkat mengungkapkan terlalu banyak tentang representasi tipe asli yang sedang didefinisikan. Sebagai gantinya, pertimbangkan untuk membungkus singkatan dalam jenis kelas atau penyatuan diskriminasi kasus tunggal (atau, ketika performa sangat penting, pertimbangkan untuk menggunakan jenis struct untuk membungkus singkatan).
Misalnya, menggoda untuk mendefinisikan multi-peta sebagai kasus khusus dari peta F#, contohnya:
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
Namun, operasi notasi titik logis pada jenis ini tidak sama dengan operasi pada Map – misalnya, wajar bahwa operator pencarian map[key]
mengembalikan daftar kosong jika kunci tidak ada dalam kamus, daripada menimbulkan pengecualian.
Panduan pustaka untuk Penggunaan dari Bahasa-bahasa .NET lainnya
Saat merancang pustaka untuk digunakan dari bahasa .NET lainnya, penting untuk mematuhi Pedoman Desain Pustaka .NET . Dalam dokumen ini, pustaka ini diberi label sebagai pustaka vanilla .NET, dibandingkan dengan pustaka F#-facing yang menggunakan konstruksi F# tanpa batasan. Merancang pustaka vanilla .NET berarti menyediakan API yang familiar dan idiomatik yang konsisten dengan bagian lain dari .NET Framework dengan meminimalkan penggunaan konstruksi yang spesifik untuk F# dalam API publik. Aturan dijelaskan di bagian berikut.
Desain Namespace dan Tipe (untuk pustaka yang digunakan oleh bahasa .NET lainnya)
Terapkan konvensi penamaan .NET ke API publik komponen Anda
Beri perhatian khusus pada penggunaan nama singkatan dan pedoman kapitalisasi .NET.
type pCoord = ...
member this.theta = ...
type PolarCoordinate = ...
member this.Theta = ...
Menggunakan namespace, jenis, dan anggota sebagai struktur organisasi utama untuk komponen Anda
Semua file yang berisi fungsionalitas publik harus dimulai dengan deklarasi namespace
, dan satu-satunya entitas publik dalam namespace harus tipe. Jangan gunakan modul F#.
Gunakan modul non-publik untuk menyimpan kode implementasi, jenis utilitas, dan fungsi utilitas.
Jenis statis harus lebih disukai daripada modul, karena memungkinkan evolusi API di masa mendatang menggunakan kelebihan beban dan konsep desain .NET API lainnya yang mungkin tidak digunakan dalam modul F#.
Misalnya, sebagai ganti API publik berikut:
module Fabrikam
module Utilities =
let Name = "Bob"
let Add2 x y = x + y
let Add3 x y z = x + y + z
Pertimbangkan sebagai gantinya:
namespace Fabrikam
[<AbstractClass; Sealed>]
type Utilities =
static member Name = "Bob"
static member Add(x,y) = x + y
static member Add(x,y,z) = x + y + z
Gunakan tipe rekaman F# di API vanilla .NET jika desain tipe tidak akan berkembang
Jenis rekaman F# dikompilasi ke kelas .NET sederhana. Ini cocok untuk beberapa jenis sederhana dan stabil dalam API. Pertimbangkan untuk menggunakan atribut [<NoEquality>]
dan [<NoComparison>]
untuk menekan generasi antarmuka otomatis. Hindari menggunakan bidang rekaman yang dapat diubah di API .NET vanilla karena ini mengekspos bidang publik. Selalu pertimbangkan apakah kelas akan memberikan opsi yang lebih fleksibel untuk evolusi API di masa mendatang.
Misalnya, kode F# berikut mengekspos API publik ke konsumen C#:
F#:
[<NoEquality; NoComparison>]
type MyRecord =
{ FirstThing: int
SecondThing: string }
C#:
public sealed class MyRecord
{
public MyRecord(int firstThing, string secondThing);
public int FirstThing { get; }
public string SecondThing { get; }
}
Sembunyikan representasi tipe gabungan F# di API .NET standar
Jenis tipe union F# umumnya tidak digunakan di seluruh batas komponen, bahkan untuk pemrograman F#-ke-F#. Mereka adalah perangkat implementasi yang sangat baik ketika digunakan secara internal dalam komponen dan pustaka.
Saat merancang API .NET vanilla, pertimbangkan untuk menyembunyikan representasi tipe penyatuan dengan menggunakan deklarasi privat atau berkas tanda tangan.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
Anda juga dapat menambah tipe yang menggunakan representasi union secara internal dengan anggota untuk menyediakan API yang diinginkan dan ditujukan untuk .NET.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
/// A public member for use from C#
member x.Evaluate =
match x with
| And(a,b) -> a.Evaluate && b.Evaluate
| Not a -> not a.Evaluate
| True -> true
/// A public member for use from C#
static member CreateAnd(a,b) = And(a,b)
Mendesain GUI dan komponen lain menggunakan pola desain kerangka kerja
Ada banyak kerangka kerja berbeda yang tersedia dalam .NET, seperti WinForms, WPF, dan ASP.NET. Konvensi penamaan dan desain untuk masing-masing harus digunakan jika Anda merancang komponen untuk digunakan dalam kerangka kerja ini. Misalnya, untuk pemrograman WPF, adopsi pola desain WPF untuk kelas yang Anda rancang. Untuk model dalam pemrograman antarmuka pengguna, gunakan pola desain seperti peristiwa dan koleksi berbasis pemberitahuan seperti yang ditemukan di System.Collections.ObjectModel.
Desain Objek dan Anggota (untuk pustaka yang digunakan oleh bahasa .NET lainnya)
Gunakan atribut CLIEvent untuk mengekspos peristiwa .NET
Buat DelegateEvent
dengan jenis delegasi .NET tertentu yang mengambil objek dan EventArgs
(bukan Event
, yang hanya menggunakan jenis FSharpHandler
secara default) sehingga peristiwa dipublikasikan dengan cara yang dikenal dalam bahasa .NET lainnya.
type MyBadType() =
let myEv = new Event<int>()
[<CLIEvent>]
member this.MyEvent = myEv.Publish
type MyEventArgs(x: int) =
inherit System.EventArgs()
member this.X = x
/// A type in a component designed for use from other .NET languages
type MyGoodType() =
let myEv = new DelegateEvent<EventHandler<MyEventArgs>>()
[<CLIEvent>]
member this.MyEvent = myEv.Publish
Mengekspos operasi asinkron sebagai metode yang mengembalikan tugas berbasis .NET
Tugas digunakan dalam .NET untuk mewakili komputasi asinkron aktif. Tugas secara umum kurang bersifat komposisional dibandingkan dengan objek F# Async<T>
, karena mewakili tugas yang sudah dieksekusi dan tidak dapat dikombinasikan dengan cara yang memungkinkan komposisi paralel, atau yang menyembunyikan proses penyebaran sinyal pembatalan dan parameter kontekstual lainnya.
Namun, terlepas dari hal ini, metode yang mengembalikan Tugas adalah representasi standar pemrograman asinkron pada .NET.
/// A type in a component designed for use from other .NET languages
type MyType() =
let compute (x: int): Async<int> = async { ... }
member this.ComputeAsync(x) = compute x |> Async.StartAsTask
Anda juga akan sering ingin menerima token pembatalan eksplisit:
/// A type in a component designed for use from other .NET languages
type MyType() =
let compute(x: int): Async<int> = async { ... }
member this.ComputeAsTask(x, cancellationToken) = Async.StartAsTask(compute x, cancellationToken)
Gunakan jenis delegasi .NET alih-alih jenis fungsi F#
Di sini "tipe fungsi F#" merujuk pada "jenis tipe panah" seperti int -> int
.
Alih-alih ini:
member this.Transform(f: int->int) =
...
Lakukan ini:
member this.Transform(f: Func<int,int>) =
...
Tipe fungsi F# muncul sebagai class FSharpFunc<T,U>
untuk bahasa .NET lainnya dan tidak terlalu sesuai untuk fitur dan alat bahasa yang memahami tipe delegasi. Saat mengembangkan metode tingkat tinggi yang menargetkan .NET Framework 3.5 atau yang lebih tinggi, delegasi System.Func
dan System.Action
adalah API yang tepat untuk diterbitkan agar pengembang .NET dapat menggunakan API ini secara efisien. (Saat menargetkan .NET Framework 2.0, jenis delegasi yang ditentukan sistem lebih terbatas; pertimbangkan untuk menggunakan jenis delegasi yang telah ditentukan sebelumnya seperti System.Converter<T,U>
atau menentukan jenis delegasi tertentu.)
Di sisi lain, delegate .NET tidak sesuai secara alami untuk pustaka yang berfokus pada F# (lihat bagian berikutnya tentang pustaka yang berfokus pada F#). Akibatnya, strategi implementasi umum saat mengembangkan metode urutan yang lebih tinggi untuk pustaka vanilla .NET adalah untuk menulis semua implementasi menggunakan jenis fungsi F#, dan kemudian membuat API publik menggunakan delegasi sebagai fasad tipis di atas implementasi F# yang sebenarnya.
Gunakan pola TryGetValue alih-alih mengembalikan nilai opsi F#, dan lebih suka metode kelebihan beban untuk mengambil nilai opsi F# sebagai argumen
Pola umum penggunaan untuk jenis opsi F# dalam API lebih baik diimplementasikan dalam API vanilla .NET menggunakan teknik desain .NET standar. Alih-alih mengembalikan nilai opsi F#, pertimbangkan untuk menggunakan jenis pengembalian bool ditambah parameter keluar seperti dalam pola "TryGetValue". Dan alih-alih mengambil nilai opsi F# sebagai parameter, pertimbangkan untuk menggunakan metode kelebihan beban atau argumen opsional.
member this.ReturnOption() = Some 3
member this.ReturnBoolAndOut(outVal: byref<int>) =
outVal <- 3
true
member this.ParamOption(x: int, y: int option) =
match y with
| Some y2 -> x + y2
| None -> x
member this.ParamOverload(x: int) = x
member this.ParamOverload(x: int, y: int) = x + y
Gunakan tipe antarmuka koleksi .NET IEnumerable<T> dan IDictionary<Key,Value> untuk parameter serta nilai pengembalian
Hindari penggunaan jenis koleksi konkret seperti array .NET T[]
, jenis F# list<T>
, Map<Key,Value>
dan Set<T>
, dan jenis koleksi konkret .NET seperti Dictionary<Key,Value>
. Panduan Desain Pustaka .NET memiliki saran yang baik mengenai kapan harus menggunakan berbagai jenis koleksi seperti IEnumerable<T>
. Beberapa penggunaan array (T[]
) dapat diterima dalam kondisi tertentu, demi kinerja. Perhatikan terutama bahwa seq<T>
hanyalah alias F# untuk IEnumerable<T>
, dan dengan demikian seq sering kali merupakan jenis yang sesuai untuk API .NET vanilla.
Alih-alih daftar F#:
member this.PrintNames(names: string list) =
...
Gunakan urutan F#:
member this.PrintNames(names: seq<string>) =
...
Gunakan Tipe Unit sebagai satu-satunya jenis input metode untuk menentukan metode tanpa argumen, atau sebagai satu-satunya jenis pengembalian untuk menentukan metode yang tidak mengembalikan nilai
Hindari penggunaan lain dari jenis satuan. Ini bagus:
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
Ini buruk:
member this.WrongUnit( x: unit, z: int) = ((), ())
Periksa nilai null pada batas VANILLA .NET API
Kode implementasi F# cenderung memiliki lebih sedikit nilai null, karena pola desain yang tidak dapat diubah dan pembatasan penggunaan literal null untuk jenis F#. Bahasa .NET lainnya sering menggunakan null sebagai nilai lebih sering. Karena itu, kode F# yang mengekspos API .NET vanilla harus memeriksa parameter untuk null di batas API, dan mencegah nilai-nilai ini mengalir lebih dalam ke dalam kode implementasi F#. Fungsi isNull
atau pencocokan pola pada pola null
dapat digunakan.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
Dimulai dengan F# 9, Anda dapat memanfaatkan sintaks | null
baru untuk membuat pengkompilasi menunjukkan kemungkinan nilai null dan di mana mereka membutuhkan penanganan:
let checkNonNull argName (arg: obj | null) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj | null) =
if isNull arg then nullArg argName
else ()
Di F# 9, pengkompilasi memancarkan peringatan ketika mendeteksi bahwa kemungkinan nilai null tidak ditangani:
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
// `ReadLine` may return null here - when the stream is finished
let line = sr.ReadLine()
// nullness warning: The types 'string' and 'string | null'
// do not have equivalent nullability
printLineLength line
Peringatan ini sebaiknya ditangani menggunakan pola null F# dalam proses pencocokan.
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
let line = sr.ReadLine()
match line with
| null -> ()
| s -> printLineLength s
Hindari penggunaan tuple sebagai nilai pengembalian
Sebagai gantinya, disarankan untuk mengembalikan tipe bernama yang menyimpan data agregat, atau menggunakan parameter keluar (out parameter) untuk mengembalikan beberapa nilai. Meskipun tuple dan tuple struct ada di .NET (termasuk dukungan bahasa C# untuk tuple struct), mereka paling sering tidak menyediakan API yang ideal dan diharapkan untuk pengembang .NET.
Hindari penggunaan teknik currying pada parameter
Sebagai gantinya, gunakan konvensi panggilan .NET Method(arg1,arg2,…,argN)
.
member this.TupledArguments(str, num) = String.replicate num str
Tip: Jika Anda merancang pustaka yang dapat digunakan dalam bahasa .NET apa pun, maka tidak ada yang lebih baik daripada benar-benar melakukan beberapa pemrograman C# dan Visual Basic eksperimental untuk memastikan bahwa pustaka Anda "merasa pas" dalam kedua bahasa ini. Anda juga dapat menggunakan alat seperti .NET Reflector dan Visual Studio Object Browser untuk memastikan bahwa pustaka dan dokumentasinya muncul seperti yang diharapkan oleh pengembang.
Lampiran
Contoh ujung ke ujung merancang kode F# untuk digunakan oleh bahasa .NET yang lainnya
Pertimbangkan kelas berikut:
open System
type Point1(angle,radius) =
new() = Point1(angle=0.0, radius=0.0)
member x.Angle = angle
member x.Radius = radius
member x.Stretch(l) = Point1(angle=x.Angle, radius=x.Radius * l)
member x.Warp(f) = Point1(angle=f(x.Angle), radius=x.Radius)
static member Circle(n) =
[ for i in 1..n -> Point1(angle=2.0*Math.PI/float(n), radius=1.0) ]
Jenis F# yang disimpulkan dari kelas ini adalah sebagai berikut:
type Point1 =
new : unit -> Point1
new : angle:double * radius:double -> Point1
static member Circle : n:int -> Point1 list
member Stretch : l:double -> Point1
member Warp : f:(double -> double) -> Point1
member Angle : double
member Radius : double
Mari kita lihat bagaimana tipe F# ini terlihat oleh pemrogram yang menggunakan bahasa .NET lain. Misalnya, "deklarasi" C# kira-kira adalah sebagai berikut:
// C# signature for the unadjusted Point1 class
public class Point1
{
public Point1();
public Point1(double angle, double radius);
public static Microsoft.FSharp.Collections.List<Point1> Circle(int count);
public Point1 Stretch(double factor);
public Point1 Warp(Microsoft.FSharp.Core.FastFunc<double,double> transform);
public double Angle { get; }
public double Radius { get; }
}
Ada beberapa poin penting yang perlu diperhatikan tentang bagaimana F# mewakili konstruksi di sini. Misalnya:
Metadata seperti nama argumen telah dipertahankan.
Metode F# yang mengambil dua argumen menjadi metode C# yang mengambil dua argumen.
Fungsi dan daftar menjadi referensi ke jenis terkait di pustaka F#.
Kode berikut menunjukkan cara menyesuaikan kode ini untuk memperhitungkan hal-hal ini.
namespace SuperDuperFSharpLibrary.Types
type RadialPoint(angle:double, radius:double) =
/// Return a point at the origin
new() = RadialPoint(angle=0.0, radius=0.0)
/// The angle to the point, from the x-axis
member x.Angle = angle
/// The distance to the point, from the origin
member x.Radius = radius
/// Return a new point, with radius multiplied by the given factor
member x.Stretch(factor) =
RadialPoint(angle=angle, radius=radius * factor)
/// Return a new point, with angle transformed by the function
member x.Warp(transform:Func<_,_>) =
RadialPoint(angle=transform.Invoke angle, radius=radius)
/// Return a sequence of points describing an approximate circle using
/// the given count of points
static member Circle(count) =
seq { for i in 1..count ->
RadialPoint(angle=2.0*Math.PI/float(count), radius=1.0) }
Jenis kode F# yang disimpulkan adalah sebagai berikut:
type RadialPoint =
new : unit -> RadialPoint
new : angle:double * radius:double -> RadialPoint
static member Circle : count:int -> seq<RadialPoint>
member Stretch : factor:double -> RadialPoint
member Warp : transform:System.Func<double,double> -> RadialPoint
member Angle : double
member Radius : double
Tanda tangan C# sekarang sebagai berikut:
public class RadialPoint
{
public RadialPoint();
public RadialPoint(double angle, double radius);
public static System.Collections.Generic.IEnumerable<RadialPoint> Circle(int count);
public RadialPoint Stretch(double factor);
public RadialPoint Warp(System.Func<double,double> transform);
public double Angle { get; }
public double Radius { get; }
}
Perbaikan yang dibuat untuk menyiapkan jenis ini untuk digunakan sebagai bagian dari pustaka vanilla .NET adalah sebagai berikut:
Disesuaikan beberapa nama:
Point1
,n
,l
, danf
menjadiRadialPoint
,count
,factor
, dantransform
, masing-masing.Menggunakan jenis pengembalian
seq<RadialPoint>
alih-alihRadialPoint list
dengan mengubah konstruksi daftar menggunakan[ ... ]
ke konstruksi urutan menggunakanIEnumerable<RadialPoint>
.Menggunakan jenis delegasi .NET
System.Func
alih-alih jenis fungsi F#.
Ini membuatnya jauh lebih baik untuk digunakan dalam kode C#.