Ответ 1
Сначала давайте посмотрим на документацию по перегруженным методам, о которой не так много сказать:
Перегруженные методы - это методы, имеющие одинаковые имена в заданном типе, но имеющие разные аргументы. В 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
параметра static member DivRem
здесь генерировалось по строке:
(^T: (static member DivRem: _ * _ -> _ -> _) (D, d, &r)), r
Это происходит из-за того, как компилятор F # обрабатывает вызовы функций out
параметров. В С# статический метод DivRem
, который здесь просматривается, является параметром (a, b, out c)
. Компилятор F # преобразует эту подпись в подпись (a, b) → c
. Таким образом, это ограничение типа ищет статический метод, например BigInteger.DivRem
и вызывает его с параметрами (D, d, &r)
где &r
в F # похоже out r
в С#. Результатом этого вызова является частное, и он присваивает остаток параметру out
предоставленному методу. Таким образом, эта перегрузка просто вызывает статический метод DivRem
для предоставленного типа и возвращает кортеж quotient, remainder
.
Наконец, если предоставленный тип не имеет статического метода DivRem
, то вторая перегрузка (та, которая имеет Default1
по Default1
в своей подписи), является той, которая заканчивается Default1
. Он ищет перегруженные операторы *
, -
и /
на предоставленных типах и использует их для вычисления коэффициента и остатка.
Другими словами, как объясняет Густаво, более короткий ответ объясняет, что класс DivRem будет следовать следующей логике (в компиляторе):
- Если для используемых типов существует статический метод
DivRem
, вызовите его, поскольку он предположил, что он может быть оптимизирован для этого типа. - В противном случае вычислите фактор
q
какD/d
, затем вычислите остаток какD - q * d
.
То, что это: остальная сложность заключается в том, чтобы заставить компилятор F # поступать правильно, и в итоге divRem
функция divRem
, максимально эффективная.