Directrices de diseño de componentes de F#
Este documento es un conjunto de directrices de diseño de componentes para la programación en F#, basadas en las Directrices de diseño de componentes de F#, v14, Microsoft Research, y una versión que originalmente fue curada y mantenida por la F# Software Foundation.
En este documento se supone que está familiarizado con la programación de F#. Muchas gracias a la comunidad de F# por sus contribuciones y comentarios útiles sobre varias versiones de esta guía.
Visión general
En este documento se examinan algunos de los problemas relacionados con el diseño y la codificación de componentes de F#. Un componente puede significar cualquiera de los siguientes elementos:
- Una capa del proyecto de F# que tiene consumidores externos dentro de ese proyecto.
- Una biblioteca diseñada para su consumo por código de F# a través de los límites del ensamblado.
- Una biblioteca diseñada para su consumo por cualquier lenguaje .NET a través de los límites del ensamblado.
- Una biblioteca diseñada para la distribución a través de un repositorio de paquetes, como NuGet.
Las técnicas que se describen en este artículo siguen los cinco principios del buen código de F#y, por tanto, usan la programación funcional y de objetos según corresponda.
Independientemente de la metodología, el diseñador de componentes y bibliotecas se enfrenta a una serie de problemas prácticos y prosaicos al intentar crear una API que los desarrolladores puedan usar más fácilmente. La aplicación consciente de las directrices de diseño de la biblioteca .NET le ayudará a crear un conjunto coherente de API que son agradables de consumir.
Directrices generales
Hay algunas directrices universales que se aplican a las bibliotecas de F#, independientemente del público previsto para la biblioteca.
Obtenga información sobre las directrices de diseño de la biblioteca .NET.
Independientemente del tipo de codificación de F# que esté haciendo, es útil tener conocimientos prácticos de las directrices de diseño de biblioteca de .NET . La mayoría de los demás programadores de F# y .NET estarán familiarizados con estas directrices y esperan que el código de .NET se ajuste a ellas.
Las directrices de diseño de la biblioteca .NET proporcionan instrucciones generales sobre la nomenclatura, el diseño de clases e interfaces, el diseño de miembros (propiedades, métodos, eventos, etc.) y mucho más, y son un primer punto de referencia útil para una variedad de instrucciones de diseño.
Añade comentarios de documentación XML a tu código
La documentación XML sobre las API públicas garantiza que los usuarios puedan obtener excelentes IntelliSense e Quickinfo al usar estos tipos y miembros, y habilitar la creación de archivos de documentación para la biblioteca. Consulta la documentación XML sobre varias etiquetas xml que se pueden usar para el marcado adicional dentro de los comentarios xmldoc.
/// A class for representing (x,y) coordinates
type Point =
/// Computes the distance between this point and another
member DistanceTo: otherPoint:Point -> float
Puede usar los comentarios XML de forma abreviada (/// comment
) o los comentarios XML estándar (///<summary>comment</summary>
).
Considere la posibilidad de usar archivos de firma explícitos (.fsi) para las API estables de biblioteca y componente
El uso de archivos de firmas explícitas en una biblioteca de F# proporciona un resumen concisa de la API pública, que ayuda a garantizar que conoce la superficie pública completa de la biblioteca y proporciona una separación limpia entre la documentación pública y los detalles de implementación internos. Los archivos de firma agregan fricción al cambiar la API pública, ya que requieren que se realicen cambios en los archivos de implementación y firma. Como resultado, los archivos de firma normalmente solo se deben introducir cuando una API se ha solidificado y ya no se espera que cambie significativamente.
Siga los procedimientos recomendados para usar cadenas en .NET.
Siga los procedimientos recomendados para usar cadenas en instrucciones de .NET cuando el ámbito del proyecto lo garantiza. En concreto, indica explícitamente intención cultural en la conversión y comparación de cadenas (si procede).
Directrices para bibliotecas orientadas a F#
En esta sección se presentan recomendaciones para desarrollar bibliotecas públicas orientadas a F#; es decir, las bibliotecas que exponen las API públicas que están diseñadas para ser consumidas por los desarrolladores de F#. Hay una variedad de recomendaciones de diseño de biblioteca aplicables específicamente a F#. En ausencia de las recomendaciones específicas siguientes, las Directrices de diseño de la biblioteca .NET son la guía de referencia.
Convenciones de nomenclatura
Uso de convenciones de nomenclatura y mayúsculas de .NET
En la tabla siguiente se siguen las convenciones de nomenclatura y mayúsculas de .NET. También hay pequeñas adiciones para incluir construcciones de F#. Esas recomendaciones están especialmente pensadas para APIs que trascienden los límites de F# a F#, y se ajustan a los idiomas de la BCL de .NET y de la mayoría de las bibliotecas.
Construir | Caso | Parte | Ejemplos | Notas |
---|---|---|---|---|
Tipos concretos | PascalCase | Sustantivo/ adjetivo | List, Double, Complex | Los tipos concretos son estructuras, clases, enumeraciones, delegados, registros y uniones. Aunque los nombres de tipo tradicionalmente están en minúsculas en OCaml, F# ha adoptado el esquema de nomenclatura de .NET para los tipos. |
DLL | PascalCase | Fabrikam.Core.dll | ||
Etiquetas de sindicato | PascalCase | Nombre | Algunos, Agregar, Correcto | No use un prefijo en las API públicas. Opcionalmente, use un prefijo cuando sea para uso interno, como "tipo Teams = TAlpha | TBeta | TDelta". |
Evento | PascalCase | Verbo | ValueChanged/ValueChanging | |
Excepciones | PascalCase | WebException | El nombre debe terminar con "Exception". | |
Campo | PascalCase | Nombre | CurrentName | |
Tipos de interfaz | PascalCase | Sustantivo/ adjetivo | IDisposable | El nombre debe empezar por "I". |
Método | PascalCase | Verbo | ToString | |
Namespace | PascalCase | Microsoft.FSharp.Core | Por lo general, use <Organization>.<Technology>[.<Subnamespace>] , aunque omita la organización si la tecnología es independiente de esta. |
|
Parámetros | camelCase | Nombre | tipoNombre, transformar, rango | |
let values (interno) | camelCase o PascalCase | Sustantivo/ verbo | getValue, myTable | |
let values (externo) | camelCase o PascalCase | Sustantivo/verbo | List.map, Dates.Today | Los valores enlazados a let son a menudo públicos cuando se siguen los patrones de diseño funcional tradicionales. Sin embargo, por lo general, use PascalCase cuando el identificador se pueda usar desde otros lenguajes de .NET. |
Propiedad | PascalCase | Sustantivo/ adjetivo | IsEndOfFile, BackColor | Las propiedades booleanas suelen usar Is y Can y deben ser afirmativas, como en IsEndOfFile, no IsNotEndOfFile. |
Evitar abreviaturas
Las instrucciones de .NET desaconsejan el uso de abreviaturas (por ejemplo, "use OnButtonClick
en lugar de OnBtnClick
"). Se toleran las abreviaturas comunes, como Async
para "Asincrónico". Esta guía a veces se omite para la programación funcional; por ejemplo, List.iter
usa una abreviatura de "iteración". Por este motivo, el uso de abreviaturas tiende a tolerarse en mayor medida en la programación de F# a F#, pero debe evitarse, generalmente, en el diseño de componentes públicos.
Evitar conflictos de nombres de mayúsculas y minúsculas
Las instrucciones de .NET dicen que no se puede usar solo el uso de mayúsculas y minúsculas para la desambiguación de los conflictos de nombres, ya que algunos lenguajes de cliente (por ejemplo, Visual Basic) no distinguen mayúsculas de minúsculas.
Usar acrónimos cuando corresponda
Los acrónimos como XML no son abreviaturas y se usan comúnmente en las bibliotecas de .NET en minúsculas (Xml). Solo se deben usar acrónimos conocidos y ampliamente reconocidos.
Uso de PascalCase para nombres de parámetro genéricos
Use PascalCase para los nombres de parámetro genérico en las API públicas, incluido para las bibliotecas orientadas a F#. En concreto, use nombres como T
, U
, T1
, T2
para parámetros genéricos arbitrarios y, cuando los nombres específicos tengan sentido, las bibliotecas orientadas a F#usan nombres como Key
, Value
, Arg
(pero no por ejemplo, TKey
).
Uso de PascalCase o camelCase para funciones y valores públicos en módulos de F#
camelCase se usa para funciones públicas diseñadas para usarse sin calificar (por ejemplo, invalidArg
) y para las «funciones de colección estándar» (por ejemplo, List.map). En ambos casos, los nombres de función actúan como palabras clave en el lenguaje.
Diseño de objeto, tipo y módulo
Utiliza espacios de nombres o módulos para contener tus tipos y módulos
Cada archivo F# de un componente debe comenzar con una declaración de espacio de nombres o una declaración de módulo.
namespace Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
o
module Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
Las diferencias entre el uso de módulos y espacios de nombres para organizar el código en el nivel superior son las siguientes:
- Los espacios de nombres pueden abarcar varios archivos
- Los espacios de nombres no pueden contener funciones de F# a menos que estén dentro de un módulo interno
- El código de cualquier módulo determinado debe estar contenido dentro de un único archivo.
- Los módulos de nivel superior pueden contener funciones de F# sin necesidad de un módulo interno
La elección entre un espacio de nombres de primer nivel o un módulo afecta al formato compilado del código y, por lo tanto, afectará la visualización desde otros lenguajes .NET si la API finalmente se consuma afuera del código de F#.
Usar métodos y propiedades para las operaciones intrínsecas a los tipos de objeto
Al trabajar con objetos, es mejor asegurarse de que la funcionalidad consumible se implemente como métodos y propiedades en dicho tipo.
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) = ...
La mayor parte de la funcionalidad de un miembro determinado no tiene por qué implementarse necesariamente en ese miembro, pero la parte consumible de esa funcionalidad sí debe implementarse.
Uso de clases para encapsular el estado mutable
En F#, esto solo debe realizarse cuando ese estado aún no esté encapsulado por otra construcción de lenguaje, como un cierre, una expresión de secuencia o un cálculo asincrónico.
type Counter() =
// let-bound values are private in classes.
let mutable count = 0
member this.Next() =
count <- count + 1
count
Uso de interfaces para agrupar operaciones relacionadas
Use tipos de interfaz para representar un conjunto de operaciones. Esto es preferible a otras opciones, como tuplas de funciones o registros de funciones.
type Serializer =
abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T
En preferencia a:
type Serializer<'T> = {
Serialize: bool -> 'T -> string
Deserialize: bool -> string -> 'T
}
Las interfaces son conceptos de primera clase en .NET, que usted puede utilizar para lograr lo que los funtores normalmente le darían. Además, se pueden usar para codificar tipos existenciales en tu programa, lo cual los registros de funciones no pueden hacer.
Uso de un módulo para agrupar funciones que actúan en colecciones
Al definir un tipo de colección, considere la posibilidad de proporcionar un conjunto estándar de operaciones como CollectionType.map
y CollectionType.iter
) para los nuevos tipos de colección.
module CollectionType =
let map f c =
...
let iter f c =
...
Si incluye este módulo, siga las convenciones de nomenclatura estándar para las funciones que se encuentran en FSharp.Core.
Uso de un módulo para agrupar funciones comunes y canónicas, especialmente en bibliotecas matemáticas y DSL
Por ejemplo, Microsoft.FSharp.Core.Operators
es una colección abierta automáticamente de funciones de nivel superior (como abs
y sin
) proporcionadas por FSharp.Core.dll.
Del mismo modo, una biblioteca de estadísticas puede incluir un módulo con funciones erf
y erfc
, donde este módulo está diseñado para abrirse explícita o automáticamente.
Considere la posibilidad de usar RequireQualifiedAccess y aplicar cuidadosamente los atributos AutoOpen
Al agregar el atributo [<RequireQualifiedAccess>]
a un módulo, se indica que es posible que no se abra el módulo y que las referencias a los elementos del módulo requieran acceso completo explícito. Por ejemplo, el módulo Microsoft.FSharp.Collections.List
tiene este atributo.
Esto resulta útil cuando las funciones y los valores del módulo tienen nombres que probablemente entren en conflicto con los nombres de otros módulos. Requerir acceso calificado puede aumentar considerablemente la capacidad de mantenimiento a largo plazo y la evolución de una biblioteca.
Se recomienda encarecidamente tener el atributo [<RequireQualifiedAccess>]
para los módulos personalizados que amplían los proporcionados por FSharp.Core
(como Seq
, List
, Array
), ya que esos módulos se usan frecuentemente en el código F# y tienen [<RequireQualifiedAccess>]
definidos en ellos; por lo general, no se recomienda definir módulos personalizados que carecen del atributo, cuando estos módulos sombrean o extienden otros módulos que tienen el atributo .
Agregar el atributo [<AutoOpen>]
a un módulo significa que el módulo se abrirá cuando se abra el espacio de nombres contenedor. El atributo [<AutoOpen>]
también se puede aplicar a un ensamblado para indicar un módulo que se abre automáticamente cuando se hace referencia al ensamblado.
Por ejemplo, una biblioteca de estadísticas MathsHeaven.Statistics puede contener un module MathsHeaven.Statistics.Operators
que contiene funciones erf
y erfc
. Es razonable marcar este módulo como [<AutoOpen>]
. Esto significa que open MathsHeaven.Statistics
también abrirá este módulo y incorporará los nombres erf
y erfc
al ámbito. Otro buen uso de [<AutoOpen>]
es para los módulos que contienen métodos de extensión.
El uso excesivo de [<AutoOpen>]
conduce a espacios de nombres contaminados y el atributo debe usarse con cuidado. En el caso de bibliotecas específicas en dominios específicos, el uso prudente de [<AutoOpen>]
puede dar lugar a una mejor facilidad de uso.
Considere la posibilidad de definir miembros de operador en clases en las que el uso de operadores conocidos es adecuado
A veces, las clases se usan para modelar construcciones matemáticas como vectores. Cuando el dominio que se modela tiene operadores conocidos, definirlos como miembros intrínsecos a la clase es útil.
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
Esta guía corresponde a las instrucciones generales de .NET para estos tipos. Sin embargo, también puede ser importante en la codificación de F#, ya que esto permite usar estos tipos junto con funciones y métodos de F# con restricciones de miembro, como List.sumBy.
Considere usar CompiledName para proporcionar un nombre descriptivo compatible con .NET para otros usuarios de lenguajes .NET.
A veces, es posible que desee asignar un nombre a algo en un estilo para los consumidores de F# (por ejemplo, un miembro estático en minúsculas para que aparezca como si fuera una función enlazada a un módulo), pero usar un estilo diferente para el nombre cuando se compila en un ensamblado. Puede usar el atributo [<CompiledName>]
para proporcionar un estilo diferente para el código que no sea F# que consume el ensamblado.
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
Mediante el uso de [<CompiledName>]
, puede aplicar las convenciones de nombres de .NET para los consumidores del ensamblado que no usan F#.
Usa la sobrecarga de métodos para las funciones miembro; si hacerlo proporciona una API más sencilla
La sobrecarga de métodos es una herramienta eficaz para simplificar una API que puede necesitar realizar una funcionalidad similar, pero con diferentes opciones o argumentos.
type Logger() =
member this.Log(message) =
...
member this.Log(message, retryPolicy) =
...
En F#, es más común sobrecargar el número de argumentos en lugar de los tipos de argumentos.
Ocultar las representaciones de los tipos de registro y unión si es probable que el diseño de estos tipos evolucione
Evite revelar representaciones concretas de objetos. Por ejemplo, la representación concreta de DateTime valores no se revela mediante la API pública externa del diseño de la biblioteca de .NET. En tiempo de ejecución, Common Language Runtime conoce la implementación confirmada que se usará durante toda la ejecución. Sin embargo, el código compilado no recoge dependencias de la representación concreta.
Evitar el uso de la herencia de implementación para la extensibilidad
En F#, rara vez se usa la herencia de implementación. Además, las jerarquías de herencia suelen ser complejas y difíciles de cambiar cuando llegan nuevos requisitos. La implementación de herencia sigue existiendo en F# para compatibilidad y casos poco frecuentes en los que es la mejor solución a un problema, pero se deben buscar técnicas alternativas en los programas de F# al diseñar con polimorfismo en mente, como la implementación de interfaz.
Firmas de función y miembro
Usa tuplas para valores devueltos al devolver un pequeño número de varios valores no relacionados
Este es un buen ejemplo de uso de una tupla en un tipo de valor devuelto:
val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger
Para los tipos de valor devuelto que contienen muchos componentes o donde los componentes están relacionados con una sola entidad identificable, considere la posibilidad de usar un tipo con nombre en lugar de una tupla.
Uso de Async<T>
para la programación asincrónica en los límites de la API de F#
Si hay una operación sincrónica correspondiente denominada Operation
que devuelve un T
, la operación asincrónica debe denominarse AsyncOperation
si devuelve Async<T>
o OperationAsync
si devuelve Task<T>
. Para los tipos de .NET usados habitualmente que exponen métodos Begin/End, considere la posibilidad de usar Async.FromBeginEnd
para escribir métodos de extensión como fachada para proporcionar el modelo de programación asincrónico de F# a esas API de .NET.
type SomeType =
member this.Compute(x:int): int =
...
member this.AsyncCompute(x:int): Async<int> =
...
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
...
Excepciones
Consulte la sección Gestión de errores para obtener información sobre el uso adecuado de excepciones, resultados y opciones.
Miembros de extensión
Aplicar cuidadosamente miembros de extensión de F# en componentes de F#a F#
Por lo general, los miembros de extensión de F# solo se deben usar para las operaciones que están en el cierre de operaciones intrínsecas asociadas a un tipo en la mayoría de sus modos de uso. Un uso común es proporcionar API más idiomáticas a F# para varios tipos de .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
Tipos de unión
Uso de uniones discriminadas en lugar de jerarquías de clases para datos estructurados en árbol
Las estructuras de tipo árbol se definen de forma recursiva. Esto es incómodo con la herencia, pero elegante con uniones discriminadas.
type BST<'T> =
| Empty
| Node of 'T * BST<'T> * BST<'T>
La representación de datos de tipo árbol con uniones discriminadas también le permite beneficiarse de la exhaustividad en la coincidencia de patrones.
Usa [<RequireQualifiedAccess>]
en tipos de unión cuyos nombres de mayúsculas y minúsculas no son suficientemente únicos
Es posible que se encuentre en un dominio donde el mismo nombre es el mejor nombre para diferentes cosas, como casos de unión discriminada. Puedes usar [<RequireQualifiedAccess>]
para eliminar la ambigüedad de los nombres de mayúsculas y minúsculas con el fin de evitar que se desencadenen errores confusos debido a la sombra que depende del orden de las instruccionesopen
Oculta las representaciones de uniones discriminadas para las API compatibles con binarios si es probable que el diseño de estos tipos evolucione
Los tipos de uniones se basan en formularios de coincidencia de patrones de F# para un modelo de programación conciso. Como se mencionó anteriormente, debe evitar revelar representaciones de datos concretas si es probable que el diseño de estos tipos evolucione.
Por ejemplo, la representación de una unión discriminada se puede ocultar mediante una declaración privada o interna, o mediante un archivo de firma.
type Union =
private
| CaseA of int
| CaseB of string
Si revelas uniones discriminadas indiscriminadamente, es posible que te resulte difícil versionar la biblioteca sin interrumpir el código de usuario. En su lugar, considera la posibilidad de revelar uno o varios patrones activos para permitir la coincidencia de patrones en los valores del tipo.
Los patrones activos proporcionan una manera alternativa de proporcionar a los consumidores de F# coincidencia de patrones, a la vez que evitan exponer directamente los tipos de unión de F#.
Funciones insertadas y restricciones de miembro
Definición de algoritmos numéricos genéricos mediante funciones insertadas con restricciones de miembro implícitas y tipos genéricos resueltos estáticamente
Las restricciones de miembro aritmético y las restricciones de comparación de F# son un estándar para la programación de F#. Por ejemplo, considere el código siguiente:
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
El tipo de esta función es el siguiente:
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
Se trata de una función adecuada para una API pública en una biblioteca matemática.
Evite el uso de restricciones de miembro para simular clases de tipo y duck typing
Es posible el «duck typing» mediante restricciones de miembro de F#. Sin embargo, los miembros que hacen uso de esto no deben usarse en general en diseños de biblioteca de F#a F#. Esto se debe a que los diseños de biblioteca basados en restricciones implícitas desconocidas o no estándar tienden a hacer que el código de usuario sea inflexible y se vincule a un patrón de marco determinado.
Además, existe una buena posibilidad de que el uso intensivo de restricciones de miembro de esta manera pueda dar lugar a tiempos de compilación muy largos.
Definiciones de operador
Evitar definir operadores simbólicos personalizados
Los operadores personalizados son esenciales en algunas situaciones y son dispositivos notacionales muy útiles en un amplio cuerpo de código de implementación. Para los nuevos usuarios de una biblioteca, las funciones con nombre suelen ser más fáciles de usar. Además, los operadores simbólicos personalizados pueden ser difíciles de documentar y los usuarios encuentran más difícil buscar ayuda en los operadores, debido a las limitaciones existentes en el IDE y los motores de búsqueda.
Como resultado, es mejor publicar la funcionalidad como funciones con nombre y miembros, y además exponer operadores para esta funcionalidad solo si las ventajas de la anotación superan la documentación y el coste cognitivo de tenerlas.
Unidades de medida
Use cuidadosamente unidades de medida para la seguridad de tipos agregada en el código de F#
Se borra información adicional de escritura para unidades de medida cuando se ven en otros lenguajes de .NET. Ten en cuenta que los componentes, las herramientas y la reflexión de .NET verán types-sans-units. Por ejemplo, los consumidores de C# verán float
en lugar de float<kg>
.
Abreviaturas de tipo
Use cuidadosamente las abreviaturas de tipo para simplificar el código de F#
Los componentes, las herramientas y la reflexión de .NET no verán nombres abreviados para los tipos. El uso significativo de las abreviaturas de tipo también puede hacer que un dominio parezca más complejo de lo que realmente es, lo que podría confundir a los consumidores.
Evite las abreviaturas de tipo para los tipos públicos cuyos miembros y propiedades deben ser intrínsecamente diferentes a los disponibles en el tipo que se abrevia.
En este caso, el tipo que se abrevia revela demasiado sobre la representación del tipo real que se está definiendo. En su lugar, considera la posibilidad de ajustar la abreviatura en un tipo de clase o una unión discriminada de un solo caso (o, cuando el rendimiento es esencial, considera la posibilidad de usar un tipo de estructura para ajustar la abreviatura).
Por ejemplo, es tentador definir un mapa múltiple como un caso especial de un mapa de F#, por ejemplo:
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
Sin embargo, las operaciones lógicas de notación de puntos en este tipo no son las mismas que las operaciones de un mapa; por ejemplo, es razonable que el operador de búsqueda map[key]
devuelva la lista vacía si la clave no está en el diccionario, en lugar de lanzar una excepción.
Directrices para bibliotecas para su uso desde otros lenguajes de .NET
Al diseñar bibliotecas para su uso desde otros lenguajes .NET, es importante cumplir las directrices de diseño de la biblioteca de .NET . En este documento, estas bibliotecas se etiquetan como bibliotecas estándar de .NET, en lugar de bibliotecas orientadas a F#, que usan construcciones de F# sin restricciones. Diseñar bibliotecas de .NET básicas significa proporcionar API conocidas e idiomáticas coherentes con el resto del .NET Framework, minimizando el uso de construcciones específicas de F# en la API pública. Las reglas se explican en las secciones siguientes.
Diseño de espacio de nombres y tipo (para bibliotecas para su uso desde otros lenguajes de .NET)
Aplicación de las convenciones de nomenclatura de .NET a la API pública de los componentes
Preste especial atención al uso de nombres abreviados y las directrices de capitalización de .NET.
type pCoord = ...
member this.theta = ...
type PolarCoordinate = ...
member this.Theta = ...
Utilice espacios de nombres, tipos y miembros como la estructura organizativa principal para sus componentes.
Todos los archivos que contienen funcionalidad pública deben comenzar con una declaración de namespace
y las únicas entidades orientadas al público en los espacios de nombres deben ser tipos. No use módulos de F#.
Use módulos no públicos para contener código de implementación, tipos de utilidad y funciones de utilidad.
Los tipos estáticos deben preferirse sobre los módulos, ya que permiten que la evolución futura de la API use sobrecargas y otros conceptos de diseño de la API de .NET que no se puedan usar en módulos de F#.
Por ejemplo, en lugar de la SIGUIENTE API pública:
module Fabrikam
module Utilities =
let Name = "Bob"
let Add2 x y = x + y
let Add3 x y z = x + y + z
Considere en su lugar:
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
Usar los tipos de registros de F# en las API de .NET en vainilla si el diseño de los tipos no evolucionará
Los tipos de registro de F# se compilan en una clase .NET simple. Estos son adecuados para algunos tipos simples y estables en las API. Considere la posibilidad de usar los atributos [<NoEquality>]
y [<NoComparison>]
para suprimir la generación automática de interfaces. Evite también el uso de campos de registro mutables en las API de .NET predeterminadas, ya que estos exponen un campo público. Considere siempre si una clase proporcionaría una opción más flexible para la evolución futura de la API.
Por ejemplo, el siguiente código de F# expone la API pública a un consumidor de 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; }
}
Ocultar la representación de los tipos de unión de F# en las API estándar de .NET
Los tipos de unión de F# no se usan normalmente a través de los límites del componente, incluso para la codificación de F#a F#. Son un dispositivo de implementación excelente cuando se usan internamente dentro de componentes y bibliotecas.
Al diseñar una API básica de .NET, considere ocultar la representación de un tipo unión mediante una declaración privada o un archivo de firma.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
También puedes aumentar los tipos que usan una representación de unión internamente con miembros para proporcionar un deseado. API orientada a 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)
Diseño de la GUI y otros componentes mediante los patrones de diseño del marco
Hay muchos marcos diferentes disponibles en .NET, como WinForms, WPF y ASP.NET. Las convenciones de nomenclatura y diseño para cada una de ellas deben usarse si está diseñando componentes para su uso en estos marcos. Por ejemplo, para la programación de WPF, adopte patrones de diseño de WPF para las clases que está diseñando. Para los modelos de programación de la interfaz de usuario, use patrones de diseño como eventos y colecciones basadas en notificaciones, como los que se encuentran en System.Collections.ObjectModel.
Diseño de objetos y miembros (para bibliotecas para su uso desde otros lenguajes .NET)
Uso del atributo CLIEvent para exponer eventos de .NET
Construya un DelegateEvent
con un tipo de delegado .NET específico que toma un objeto y EventArgs
(en lugar de un Event
, que simplemente usa el tipo FSharpHandler
de forma predeterminada) para que los eventos se publiquen de la manera familiar con otros lenguajes .NET.
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
Exponer operaciones asincrónicas como métodos que devuelven tareas de .NET
Las tareas se usan en .NET para representar cálculos asincrónicos activos. Las tareas son, en general, menos composicionales que los objetos F# Async<T>
, ya que representan tareas "ya en ejecución" y no se pueden componer conjuntamente de maneras que realicen composición paralela o que oculten la propagación de las señales de cancelación y otros parámetros contextuales.
Sin embargo, a pesar de esto, los métodos que devuelven Tasks son la representación estándar de la programación asincrónica en .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
Con frecuencia, también querrá aceptar un token de cancelación explícito:
/// 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)
Uso de tipos de delegado de .NET en lugar de tipos de función de F#
Aquí "Tipos de función F#" significan tipos de "flecha" como int -> int
.
En lugar de esto:
member this.Transform(f: int->int) =
...
Haga esto:
member this.Transform(f: Func<int,int>) =
...
El tipo de función F# aparece como class FSharpFunc<T,U>
en otros lenguajes .NET y es menos adecuado para las características de lenguaje y las herramientas que comprenden los tipos delegados. Al crear un método de orden superior destinado a .NET Framework 3.5 o superior, los delegados de System.Func
y System.Action
son las API adecuadas para publicar para permitir que los desarrolladores de .NET consuman estas API de forma de baja fricción. (Al tener como destino .NET Framework 2.0, los tipos de delegado definidos por el sistema son más limitados; considere la posibilidad de usar tipos delegados predefinidos, como System.Converter<T,U>
o definir un tipo de delegado específico).
En el lado inverso, los delegados de .NET no son naturales para las bibliotecas orientadas a F#(consulte la sección siguiente sobre las bibliotecas orientadas a F#). Como resultado, una estrategia de implementación común al desarrollar métodos de orden superior en bibliotecas .NET básicas es crear toda la implementación mediante tipos de función F#, y a continuación, crear la API pública utilizando delegados como una capa delgada sobre la implementación real de F#.
Use el patrón TryGetValue en lugar de devolver valores de opción en F#, y prefiera la sobrecarga de métodos en lugar de recibir valores de opción de F# como argumentos.
Los patrones comunes de uso para el tipo "option" de F# en las API se implementan mejor en las API básicas de .NET mediante técnicas de diseño estándar de .NET. En lugar de devolver un valor de opción de F#, considera la posibilidad de usar el tipo de valor devuelto bool más un parámetro out como en el patrón «TryGetValue». Y en lugar de tomar valores de opción de F# como parámetros, considera la posibilidad de usar la sobrecarga de métodos o argumentos opcionales.
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
Usar los tipos de interfaz de colección de .NET IEnumerable<T> e IDictionary<Key,Value> para parámetros y valores devueltos
Evite el uso de tipos de colección concretos, como matrices de .NET T[]
, tipos de F# list<T>
, Map<Key,Value>
y Set<T>
, y tipos de colección concreta de .NET, como Dictionary<Key,Value>
. Las directrices de diseño de la biblioteca .NET tienen buenos consejos sobre cuándo usar varios tipos de colección, como IEnumerable<T>
. Algunos usos de matrices (T[]
) son aceptables en algunas circunstancias, por motivos de rendimiento. Tenga en cuenta especialmente que seq<T>
es solo el alias de F# para IEnumerable<T>
y, por tanto, seq suele ser un tipo adecuado para una API estándar de .NET.
En lugar de listas de F#:
member this.PrintNames(names: string list) =
...
Use secuencias de F#:
member this.PrintNames(names: seq<string>) =
...
Usa el tipo de unidad como el único tipo de entrada de un método para definir un método de argumento cero, o como el único tipo de valor devuelto para definir un método que devuelva void
Evite otros usos del tipo de unidad. Estos son buenos:
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
Esto es malo:
member this.WrongUnit( x: unit, z: int) = ((), ())
Comprueba los valores nulos en los límites de la API estándar de .NET
El código de implementación de F# tiende a tener menos valores NULL, debido a patrones de diseño inmutables y restricciones en el uso de literales NULL para tipos F#. Otros lenguajes .NET suelen usar null como valor con mucha más frecuencia. Por este motivo, el código de F# que expone una API estándar de .NET debe verificar que los parámetros no sean nulos en la interfaz de la API y evitar que estos valores se propaguen dentro del código de implementación de F#. Se puede usar la función isNull
o la coincidencia de patrones en el patrón null
.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
A partir de F# 9, puede aprovechar la nueva sintaxis de | null
para que el compilador indique posibles valores NULL y dónde necesitan ser gestionados.
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 ()
En F# 9, el compilador emite una advertencia cuando detecta que no se controla un valor NULL posible:
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
Estas advertencias deben solucionarse utilizando el patrón null de F# en la coincidencia:
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
Evita el uso de tuplas como valores devueltos
En su lugar, prefiere devolver un tipo con nombre que contiene los datos agregados o usar parámetros out para devolver varios valores. Aunque las tuplas y tuplas de estructura existen en .NET (incluida la compatibilidad del lenguaje C# con tuplas de estructura), a menudo no proporcionarán la API ideal y esperada para los desarrolladores de .NET.
Evitar el uso de los parámetros
En su lugar, use las convenciones de llamada de .NET Method(arg1,arg2,…,argN)
.
member this.TupledArguments(str, num) = String.replicate num str
Sugerencia: Si vas a diseñar bibliotecas para su uso desde cualquier lenguaje .NET, no hay ningún sustituto de realizar realmente alguna programación experimental de C# y Visual Basic para asegurarte de que las bibliotecas «se sienten bien» desde estos lenguajes. También puede usar herramientas como reflector de .NET y el Explorador de objetos de Visual Studio para asegurarse de que las bibliotecas y su documentación aparezcan según lo previsto para los desarrolladores.
Apéndice
Ejemplo completo de diseño de código de F# para su uso por otros lenguajes de .NET
Observa la clase siguiente:
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) ]
El tipo F# inferido de esta clase es el siguiente:
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
Echemos un vistazo a cómo aparece este tipo de F# a un programador mediante otro lenguaje .NET. Por ejemplo, la "firma" de C# aproximada es la siguiente:
// 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; }
}
Hay algunos puntos importantes que se deben tener en cuenta sobre cómo F# representa las construcciones aquí. Por ejemplo:
Se han conservado metadatos como nombres de argumento.
Los métodos de F# que toman dos argumentos se convierten en métodos de C# que toman dos argumentos.
Las funciones y las listas se convierten en referencias a los tipos correspondientes de la biblioteca de F#.
En el código siguiente se muestra cómo ajustar este código para tener en cuenta estas cosas.
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) }
El tipo F# deducido del código es el siguiente:
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
La firma de C# ahora es la siguiente:
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; }
}
Las modificaciones realizadas para preparar este tipo para su uso como parte de una biblioteca .NET básica son las siguientes:
Se ajustaron varios nombres:
Point1
,n
,l
yf
se convirtieron enRadialPoint
,count
,factor
ytransform
, respectivamente.Se utilizó un tipo de retorno
seq<RadialPoint>
en lugar deRadialPoint list
al cambiar una construcción de lista mediante[ ... ]
a una construcción de secuencia medianteIEnumerable<RadialPoint>
.Se usa el tipo de delegado .NET
System.Func
en lugar de un tipo de función F#.
Esto hace que sea mucho más agradable consumir en código de C#.