Сначала давайте посмотрим на документацию по перегруженным методам , в которой не так много сказано:
Перегруженные методы - это методы, которые имеют идентичные имена в данном типе, но имеют разные аргументы. В F # необязательные аргументы обычно используются вместо перегруженных методов. Однако перегруженные методы допускаются на языке при условии, что аргументы представлены в форме кортежа, а не в форме карри .
(Акцент мой). Причина, по которой требуется, чтобы аргументы были в форме кортежа, заключается в том, что компилятор должен знать, в точке, где вызывается функция , какая перегрузка вызывается. Например, если бы мы имели:
let f (a : int) (b : string) = printf "%d %s" a b
let f (a : int) (b : int) = printf "%d %d" a b
let g = f 5
Тогда компилятор не сможет скомпилировать функцию g
, поскольку он не будет знать на данный момент в коде , какую версию f
следует вызывать. Так что этот код будет неоднозначным.
Теперь, глядя на эти три перегруженных статических метода в классе DivRem
, у них есть три разные сигнатуры типов:
static member inline DivRem (x:^t when ^t: null and ^t: struct, y:^t, _thisClass:DivRem)
static member inline DivRem (D:'T, d:'T, [<Optional>]_impl:Default1)
static member inline DivRem (D:'T, d:'T, [<Optional>]_impl:DivRem )
В этот момент вы можете спросить себя, как компилятор будет выбирать между этими статическими перегрузками: второй и третий могут показаться неразличимыми, если третий параметр пропущен, а третий параметр задан, но является экземпляром DivRem
, тогда это выглядит неоднозначно с первой перегрузкой. На этом этапе вставка этого кода в сеанс F # Interactive может помочь, поскольку F # Interactive будет генерировать более конкретные сигнатуры типов, которые могли бы объяснить это лучше. Вот что я получил, когда вставил этот код в F # Interactive:
type DivRem =
class
inherit Default1
static member
DivRem : x: ^t * y: ^t * _thisClass:DivRem -> ^t * ^t
when ^t : null and ^t : struct
static member
DivRem : D: ^T * d: ^T * _impl:Default1 -> ^a * ^c
when ^T : (static member ( / ) : ^T * ^T -> ^a) and
( ^T or ^b) : (static member ( - ) : ^T * ^b -> ^c) and
( ^a or ^T) : (static member ( * ) : ^a * ^T -> ^b)
static member
DivRem : D: ^T * d: ^T * _impl:DivRem -> 'a * ^T
when ^T : (static member DivRem : ^T * ^T * byref< ^T> -> 'a)
static member
Invoke : D: ^T -> d: ^T -> ^T * ^T
when (DivRem or ^T) : (static member DivRem : ^T * ^T * DivRem -> ^T * ^T)
end
Первая реализация DivRem
здесь самая простая для понимания; подпись его типа такая же, как и в исходном коде FSharpPlus. Если обратиться к документации об ограничениях , то ограничения null
и struct
противоположны: ограничение null
означает «предоставленный тип должен поддерживать нулевой литерал» (исключая типы значений), а struct
ограничение означает, что «предоставленный тип должен быть типом значения .NET». Таким образом, первая перегрузка никогда не может быть выбрана; как указывает Густаво в своем превосходном ответе, он существует только для того, чтобы компилятор мог обрабатывать этот класс. (Попробуйте пропустить первую перегрузку и вызвать divRem 5m 3m
: вы обнаружите, что он не может скомпилироваться с ошибкой:
Тип «десятичный» не поддерживает оператор «DivRem»
Таким образом, первая перегрузка существует только для того, чтобы заставить F # сделать правильные вещи. Затем мы проигнорируем это и перейдем ко второй и третьей перегрузкам.
Теперь вторая и третья перегрузки различаются по типу третьего параметра. Вторая перегрузка имеет параметр, являющийся базовым классом (Default1
), а третья перегрузка имеет параметр, являющийся производным классом (DivRem
). Эти методы будут всегда вызываться с экземпляром DivRem
в качестве третьего параметра, так почему же будет выбран второй метод? Ответ заключается в автоматически сгенерированной подписи типа для третьего метода:
static member
DivRem : D: ^T * d: ^T * _impl:DivRem -> 'a * ^T
when ^T : (static member DivRem : ^T * ^T * byref< ^T> -> 'a)
Ограничение параметра static member DivRem
здесь было сгенерировано строкой:
(^T: (static member DivRem: _ * _ -> _ -> _) (D, d, &r)), r
Это происходит из-за того, как компилятор F # обрабатывает вызовы функций с параметрами out
. В C # статический метод DivRem
, который здесь ищется, - это метод с параметрами (a, b, out c)
. Компилятор F # превращает эту подпись в подпись (a, b) -> c
. Таким образом, это ограничение типа ищет статический метод, например BigInteger.DivRem
, и вызывает его с параметрами (D, d, &r)
, где &r
в F # походит на out r
в C #. Результатом этого вызова является частное, и он присваивает остаток параметру out
, заданному методу. Таким образом, эта перегрузка просто вызывает статический метод DivRem
для указанного типа и возвращает кортеж quotient, remainder
.
Наконец, если предоставленный тип не имеет статического метода DivRem
, то вторая перегрузка (с подписью Default1
) - это та, которая в итоге вызывается. Он ищет перегруженные операторы *
, -
и /
для предоставленных типов и использует их для вычисления отношения и остатка.
Другими словами, как объясняет гораздо более короткий ответ Густаво, класс DivRem здесь будет следовать следующей логике (в компиляторе):
- Если в используемых типах есть статический метод
DivRem
, вызывайте его, поскольку предполагается, что он может быть оптимизирован для этого типа.
- В противном случае, вычислить частное
q
как D / d
, а затем вычислить остаток как D - q * d
.
Вот и все: остальная сложность заключается в том, чтобы заставить компилятор F # делать правильные вещи, и в итоге получилась симпатичная divRem
функция, максимально эффективная.