Почему Math.Sqrt () является статической функцией?

31

При обсуждении статических методов и методов экземпляров я всегда думаю, что это Sqrt()должен быть метод экземпляров числовых типов, а не статический метод. Почему это? Это очевидно работает на значение.

 // looks wrong to me
 var y = Math.Sqrt(x);
 // looks better to me
 var y = x.Sqrt();

Типы значений, очевидно, могут иметь методы экземпляра, так как во многих языках есть метод экземпляра ToString().

Чтобы ответить на некоторые вопросы из комментариев: почему 1.Sqrt()не должно быть законным? 1.ToString()является.

Некоторые языки не позволяют иметь методы для типов значений, но некоторые языки могут. Я говорю об этом, в том числе Java, ECMAScript, C # и Python (с __str__(self)определением). То же относится и к другим функциям, таким как ceil()и floor()т. Д.

отстой
источник
18
На каком языке вы предлагаете это? Будет 1.sqrt()действительным?
20
Во многих языках (например, в Java) двойники являются приматами (по соображениям производительности), поэтому у них нет методов
Ричард Тингл,
45
Таким образом, числовые типы должны быть раздуты с каждой возможной математической функцией, которая может быть применена к ним?
Д Стэнли
19
FWIW Я думаю, Sqrt(x)выглядит намного более естественным, чем x.Sqrt() если бы это означало добавление функции к классу на некоторых языках, я согласен с этим. Если бы это был метод экземпляра, то x.GetSqrt()было бы более уместно указать, что он возвращает значение, а не модифицировать экземпляр.
Д Стэнли
23
Этот вопрос не может быть независимым от языка в его нынешнем виде. Это корень проблемы.
Повышение темноты

Ответы:

20

Это полностью выбор языка дизайна. Это также зависит от базовой реализации примитивных типов и соображений производительности, связанных с этим.

.NET имеет только один статический Math.Sqrtметод, который действует на a doubleи возвращает adouble . Все, что вы передаете ему, должно быть разыграно или повышено до double.

double sqrt2 = Math.Sqrt(2d);

С другой стороны, у вас есть Rust, который представляет эти операции как функции для типов :

let sqrt2 = 2.0f32.sqrt();
let higher = 2.0f32.max(3.0f32);

Но у Rust также есть универсальный синтаксис вызова функций (кто-то упоминал об этом ранее), так что вы можете выбрать все, что захотите.

let sqrt2 = f32::sqrt(2.0f32);
let higher = f32::max(2.0f32, 3.0f32);
angelsl
источник
1
Стоит отметить, что в .NET вы можете писать методы расширения, поэтому, если вы действительно хотите, чтобы эта реализация была похожа x.Sqrt(), это можно сделать. public static class DoubleExtensions { public static double Sqrt( this double self) { return Math.Sqrt(self); } }
Захари Доу
1
Также в C # 6 это может быть просто Sqrt(x) msdn.microsoft.com/en-us/library/sf0df423.aspx .
День
65

Предположим, мы разрабатываем новый язык и хотим Sqrtбыть методом экземпляра. Итак, мы смотрим на doubleкласс и начинаем проектировать. Очевидно, он не имеет входных данных (кроме экземпляра) и возвращает a double. Мы пишем и тестируем код. Совершенство.

Но получение квадратного корня из целого числа также допустимо, и мы не хотим заставлять всех переходить в двойное число, чтобы получить квадратный корень. Итак, мы переходим intи начинаем проектирование. Что это возвращает? Мы могли бы вернуть intи заставить его работать только для идеальных квадратов или округлить результат до ближайшего int(игнорируя споры о правильном методе округления на данный момент). Но что, если кто-то хочет нецелый результат? Если у нас есть два метода - один, который возвращает intи другой, который возвращает double(что невозможно в некоторых языках без изменения имени). Поэтому мы решили, что он должен вернуть double. Сейчас мы реализуем. Но реализация идентична той, которую мы использовали дляdouble, Мы копируем и вставляем? Мы приводим экземпляр к doubleи вызываем этот метод экземпляра? Почему бы не поместить логику в метод библиотеки, к которому можно получить доступ из обоих классов. Мы назовем библиотеку Mathи функцию Math.Sqrt.

Почему Math.Sqrtстатическая функция ?:

  • Поскольку реализация является одинаковой независимо от базового числового типа
  • Потому что это не влияет на конкретный экземпляр (он принимает одно значение и возвращает результат)
  • Поскольку числовые типы не зависят от этой функциональности, поэтому имеет смысл иметь ее в отдельном классе.

Мы даже не обратились к другим аргументам:

  • Должно ли оно быть названо, GetSqrtтак как оно возвращает новое значение, а не изменяет экземпляр?
  • Как насчет Square? Abs? Trunc? Log10? Ln? Power? Factorial? Sin? Cos? ArcTan?
Д Стэнли
источник
6
Не говоря уже о радостях 1.sqrt()vs 1.1.sqrt()(ghads, это выглядит ужасно), у них есть общий базовый класс? Какой контракт на его sqrt()метод?
5
@MichaelT Хороший пример. Мне потребовалось четыре чтения, чтобы понять, что 1.1.Sqrtпредставляет. Умная.
D Стэнли
17
Мне не очень понятно из этого ответа, как статический класс помогает с вашей основной причиной. Если в вашем классе Math есть double Sqrt (int) и double Sqrt (double), у вас есть два варианта: преобразовать int в double, затем вызвать в double версию или скопировать и вставить метод с соответствующими изменениями (если есть). ). Но это те же самые опции, которые вы описали для версии экземпляра. Ваши другие рассуждения (особенно ваш третий пункт) я согласен с более.
Бен Ааронсон
14
-1 этот ответ абсурдным, что же любое из этого нужно сделать с статичности? Вы получите ответы на эти же вопросы в любом случае (а «[со статической функцией] реализация та же» ложна или, по крайней мере, не более верна, чем, например, методы ...)
BlueRaja - Дэнни Пфлюгофт
20
«Реализация одинакова независимо от базового числового типа» - полная чушь. Реализации функции квадратного корня должны существенно различаться в зависимости от типа, с которым они работают, чтобы не быть ужасно неэффективными.
R ..
25

Математические операции часто очень чувствительны к производительности. Поэтому мы хотим использовать статические методы, которые могут быть полностью разрешены (и оптимизированы или встроены) во время компиляции. Некоторые языки не предлагают какого-либо механизма для определения статически отправляемых методов. Кроме того, объектная модель многих языков имеет значительные накладные расходы памяти, что недопустимо для «примитивных» типов, таких как double.

Несколько языков позволяют нам определять функции, которые используют синтаксис вызова метода, но фактически отправляются статически. Методы расширения в C # 3.0 или позже являются примером. Не виртуальные методы (например, по умолчанию для методов в C ++) - другой случай, хотя C ++ не поддерживает методы для примитивных типов. Конечно, вы можете создать свой собственный класс-оболочку в C ++, который декорирует примитивный тип различными методами, без каких-либо накладных расходов во время выполнения. Однако вам придется вручную конвертировать значения в этот тип оболочки.

Есть несколько языков, которые определяют методы для своих числовых типов. Обычно это высокодинамичные языки, где все является объектом. Здесь производительность является второстепенным фактором для концептуальной элегантности, но эти языки обычно не используются для обработки чисел. Однако эти языки могут иметь оптимизатор, который может «распаковывать» операции с примитивами.


Принимая во внимание технические соображения, мы можем рассмотреть, будет ли такой математический интерфейс на основе методов хорошим интерфейсом. Возникают две проблемы:

  • математическая запись основана на операторах и функциях, а не на методах. Такое выражение 42.sqrtбудет казаться гораздо более чуждым для многих пользователей, чем sqrt(42). Как математик, я бы предпочел возможность создавать свои собственные операторы по сравнению с синтаксисом вызова метода точек.
  • Принцип единой ответственности побуждает нас ограничивать количество операций, являющихся частью типа, основными операциями. По сравнению с умножением, квадратный корень нужен очень редко. Если ваш язык специально предназначен для статистической anlysis, а затем обеспечить более примитивов (например, операции mean, median, variance, std, normalizeчисловые списки, или гамма - функции для чисел) может быть полезным. Для языка общего назначения это просто отягощает интерфейс. Отнесение несущественных операций в отдельное пространство имен делает тип более доступным для большинства пользователей.
Амон
источник
Python является хорошим примером языка «все, что является объектом», используемого для обработки больших чисел. Скаляры NumPy на самом деле имеют десятки и десятки методов, но sqrt все еще не является одним из них. Большинство из них похожи друг на друга transposeи meanпредназначены только для обеспечения единого интерфейса с массивами NumPy, которые являются реальной рабочей лошадью.
user2357112 поддерживает Monica
7
@ user2357112: Дело в том, что сама NumPy написана на смеси C и Cython с небольшим количеством клея Python. Иначе это никогда не будет так быстро, как есть.
Кевин
1
Я думаю, что этот ответ довольно близко подходит к какому-то компромиссу, достигнутому за годы проектирования. Из других новостей, действительно ли имеет смысл в .Net иметь возможность выполнять «Hello World» .Max (), так как расширения LINQ позволяют нам И делает очень заметным в Intellisense. Бонусные баллы: каков результат? Бонус Бонус, каков результат в Unicode ...?
Andyz Smith
15

Меня мотивирует тот факт, что существует множество математических функций специального назначения, и вместо того, чтобы заполнять каждый математический тип всеми (или случайными подмножествами) тех функций, которые вы помещаете в служебный класс. В противном случае вы либо загрязните всплывающую подсказку об автозаполнении, либо заставите людей всегда искать в двух местах. (Это sinдостаточно важно, чтобы быть членом Double, или это в Mathклассе вместе с инбредами, как htanи exp1p?)

Другая практическая причина состоит в том, что оказывается, что могут быть разные способы реализации численных методов, с разной компромиссной производительностью и точностью. Ява есть Mathи есть StrictMath.

Александр Дубинский
источник
Я надеюсь, что языковые дизайнеры не заботятся об автозаполнении подсказок. А что происходит дальше Math.<^space>? Эта автозаполненная подсказка также будет загрязнена. И наоборот, я думаю, что ваш второй абзац, вероятно, один из лучших ответов здесь.
Qix
@Qix Они делают. Хотя другие люди здесь могут назвать это «деланием интерфейса раздутым».
Александр Дубинский
6

Вы правильно заметили, что здесь присутствует любопытная симметрия.

Неважно, скажу я sqrt(n)или n.sqrt()нет, они оба выражают одно и то же, и то, что вы предпочитаете, является вопросом личного вкуса, а не всего остального.

Вот почему у некоторых разработчиков языков есть веские аргументы в пользу взаимозаменяемости двух синтаксисов. Язык программирования D уже позволяет это с помощью функции, называемой Uniform Function Call Syntax . Аналогичная функция была также предложена для стандартизации в C ++ . Как отмечает Марк Эмери в комментариях , Python это тоже позволяет.

Это не без проблем. Внедрение такого фундаментального изменения синтаксиса имеет широкие последствия для существующего кода и, конечно, также является предметом спорных дискуссий среди разработчиков, которые десятилетиями обучались думать о двух синтаксисах как об описывающих разные вещи.

Я полагаю, только время покажет, возможно ли объединение двух вариантов в долгосрочной перспективе, но это, безусловно, интересное соображение.

ComicSansMS
источник
Python уже поддерживает оба этих синтаксиса. Каждый нестатический метод принимает в selfкачестве своего первого параметра, и когда вы вызываете метод как свойство экземпляра, а не как свойство класса, экземпляр неявно передается в качестве первого аргумента. Следовательно, я могу написать "foo".startswith("f")или str.startswith("foo", "f"), и я могу написать my_list.append(x)или list.append(my_list, x).
Марк Амери
@MarkAmery Хороший вопрос. Это не так радикально, как в предложениях D или C ++, но соответствует общей идее. Спасибо за указание!
ComicSansMS
3

В дополнение к ответу Д. Стенли вам нужно подумать о полиморфизме. Методы типа Math.Sqrt всегда должны возвращать одно и то же значение для одного и того же ввода. Создание статического метода - хороший способ прояснить этот момент, так как статические методы не могут быть переопределены.

Вы упомянули метод ToString (). Здесь вы можете переопределить этот метод, чтобы (под) класс был представлен другим способом как String как его родительский класс. Таким образом, вы делаете это методом экземпляра.

серп
источник
2

Ну, в Java есть обертка для каждого базового типа.
А базовые типы не являются типами классов и не имеют функций-членов.

Итак, у вас есть следующие варианты:

  1. Соберите все эти вспомогательные функции в виде проформы Math.
  2. Сделайте это статической функцией в соответствующей оболочке.
  3. Сделайте это функцией-членом в соответствующей оболочке.
  4. Измените правила Java.

Давайте исключим вариант 4, потому что ... Java - это Java, и приверженцы утверждают, что ему это нравится.

Теперь мы также можем исключить вариант 3, потому что, хотя выделение объектов довольно дешево, оно не бесплатное, и повторять это снова и снова не приходится.

Два ниже, один еще нужно убить: Вариант 2 также является плохой идеей, поскольку он означает, что каждая функция должна быть реализована для каждого типа, нельзя полагаться на расширение преобразования, чтобы заполнить пробелы, иначе несоответствия действительно повредят.
И, глядя на это java.lang.Math, есть много пробелов, особенно для типов меньше intсоответствующих double.

Итак, в конце концов, чистый победитель - первый вариант, собирая их всех в одном месте в классе полезности-функции.

Возвращаясь к варианту 4, что-то в этом направлении на самом деле произошло намного позже: вы можете попросить компилятор учитывать все статические члены любого класса, который вы хотите, при разрешении имен в течение достаточно долгого времени. import static someclass.*;

Кроме того, другие языки не имеют такой проблемы, потому что они не имеют никаких предубеждений против свободных функций (возможно, с использованием пространств имен) или гораздо меньше мелких типов.

Deduplicator
источник
1
Рассмотрим радости реализации вариаций Math.min()во всех типах оболочек.
Я считаю № 4 неубедительным. Math.sqrt()был создан в то же время, что и остальная часть Java, поэтому, когда было принято решение включить sqrt (), Mathне было исторической инерции пользователей Java, которым «так нравится». Хотя проблем с этим не так много sqrt(), перегрузочное поведение Math.round()ужасно. Возможность использовать синтаксис члена со значениями типа floatи doubleизбежала бы этой проблемы.
суперкат
2

Одна вещь, которую я не вижу в явном виде (хотя и намекает на это), заключается в том, что квадратный корень можно рассматривать как «производную» операцию: если реализация не предоставляет ее нам, мы можем написать нашу собственную.

Поскольку вопрос помечен языковым дизайном, мы могли бы рассмотреть некоторые не зависящие от языка описания. Хотя многие языки придерживаются разных принципов, в парадигмах очень распространено использование инкапсуляции для сохранения инвариантов; т.е. чтобы избежать значения, которое не ведет себя так, как подсказывает его тип.

Например, если у нас есть некоторая реализация целых чисел, использующая машинные слова, мы, вероятно, хотим как-то инкапсулировать представление (например, чтобы битовые сдвиги не могли изменить знак), но в то же время нам все еще нужен доступ к этим битам для реализации таких операций, как дополнение.

Некоторые языки могут реализовать это с помощью классов и частных методов:

class Int {
    public Int add(Int x) {
      // Do something with the bits
    }
    private List<Boolean> getBits() {
      // ...
    }
}

Некоторые с модульными системами:

signature INT = sig
  type int
  val add : int -> int -> int
end

structure Word : INT = struct
  datatype int  = (* ... *)
  fun add x y   = (* Do something with the bits *)
  fun getBits x = (* ... *)
end

Некоторые с лексической областью применения:

(defun getAdder ()
   (let ((getBits (lambda (x) ; ...
         (add     (lambda (x y) ; Do something with the bits
     'add))

И так далее. Однако ни один из этих механизмов не нужен для реализации квадратного корня: он может быть реализован с использованием открытого интерфейса числового типа и, следовательно, ему не требуется доступ к инкапсулированным деталям реализации.

Следовательно, расположение квадратного корня сводится к философии / вкусам языка и дизайнера библиотеки. Некоторые из них могут выбрать , чтобы положить его «внутри» числовые значения (например , сделать это метод экземпляра), некоторые из них могут выбрать , чтобы положить его на том же уровне, что и примитивных операций (это может означать метод экземпляра, или это может означать , живущих за пределами числовые значения, но внутри одного и того же модуля / класса / пространства имен (например, в качестве автономной функции или статического метода), некоторые могут захотеть поместить его в набор «вспомогательных» функций, некоторые могут делегировать его сторонним библиотекам.

Warbo
источник
Ничто не помешает языку разрешить вызов статического метода с использованием синтаксиса члена (как с методами расширения C # или vb.net) или с изменением последовательности члена (мне бы хотелось видеть синтаксис с двумя точками, поэтому чтобы дать Intellisense преимущества, заключающиеся в возможности перечислять только те функции, которые были пригодны для основного аргумента, но избегали неоднозначности с реальными операторами-членами).
суперкат
-2

В Java и C # ToString является методом объекта, корнем иерархии классов, поэтому каждый объект будет реализовывать метод ToString. Для типа Integer вполне естественно, что реализация ToString будет работать таким образом.

Таким образом, вы рассуждаете неправильно. Причина, по которой типы значений реализуют ToString, заключается не в том, что некоторые люди были похожи: эй, давайте иметь метод ToString для типов значений. Это потому, что ToString уже существует и это «самая естественная» вещь для вывода.

Питер Б
источник
1
Конечно, это решение, то есть решение objectиметь ToString()метод. Это в ваших словах "некоторые люди были похожи: эй, давайте иметь метод ToString для типов значений".
Residuum
-3

В отличие от String.substring, Number.sqrt на самом деле является не атрибутом числа, а новым результатом, основанным на вашем номере. Я думаю, что передача вашего числа в функцию возведения в квадрат более интуитивно понятна.

Более того, объект Math содержит другие статические члены, и имеет больше смысла собирать их вместе и использовать их единообразно.

HaLeiVi
источник
3
Встречный пример: BigInteger.pow () .