Чому 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
Тож числові типи повинні бути роздуті всіма можливими математичними функціями, які можна було б застосувати до них?
D Стенлі

19
FWIW Я думаю, що це Sqrt(x)виглядає набагато природніше, ніж x.Sqrt() якщо це означає попереджувати функцію з класом на деяких мовах, я з цим добре. Якщо це був метод примірника , то x.GetSqrt()було б більш доречно , щоб вказати , що це повернення значення , а не модифікації примірника.
D Стенлі

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);

1
Варто зазначити, що в .NET ви можете писати методи розширення, тому якщо ви дійсно хочете, щоб ця реалізація виглядала так x.Sqrt(), це можна зробити. public static class DoubleExtensions { public static double Sqrt( this double self) { return Math.Sqrt(self); } }
Zachary Dow

1
Також в C # 6 це може бути просто Sqrt(x) msdn.microsoft.com/en-us/library/sf0df423.aspx .
День

65

Припустимо, ми розробляємо нову мову і хочемо Sqrtбути методом екземпляра. Тож ми дивимось на doubleклас і починаємо проектувати. Він, очевидно, не має входів (крім екземпляра) і повертає a double. Пишемо і тестуємо код. Вдосконалення.

Але прийняття квадратного кореня з цілого числа також є дійсним, і ми не хочемо змушувати всіх перетворюватись у подвійне, просто щоб взяти квадратний корінь. Тож ми переходимо до intта починаємо проектувати. Що воно повертає? Ми можемо повернути intта зробити так, щоб він працював лише на ідеальні квадрати, або intокруглив результат до найближчого (ігноруючи дискусію про правильний метод округлення на даний момент). Але що робити, якщо хтось хоче нецілого результату? Якщо у нас є два способи - один, який повертає an, intі той, що повертає a double(що в деяких мовах неможливо без зміни назви). Тому ми вирішуємо, що він повинен повернути a double. Тепер ми реалізуємо. Але реалізація ідентична тій, яку ми використовувалиdouble. Чи копіюємо та вставляємо? Ми кидаємо екземпляр на a 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 є подвійний Sqrt (int) та double Sqrt (double), у вас є два варіанти: перетворити int у подвійний, потім зателефонувати у подвійну версію або скопіювати та вставити метод із відповідними змінами (якщо такі є) ). Але це абсолютно ті самі варіанти, які ви описали для версії екземпляра. Ваші інші міркування (особливо ваша третя точка від кулі) я погоджуюся з більшістю.
Ben Aaronson

14
-1 ця відповідь абсурдна, що стосується будь-якого з цього статичного? У будь-якому випадку у вас будуть вирішувати відповіді на ті самі запитання (і "[зі статичною функцією] реалізація однакова" є помилковою, або, принаймні, не більш вірною, ніж це було б, наприклад, методами ..)
BlueRaja - Danny Pflughoeft

20
"Реалізація однакова, незалежно від основного числового типу" - це абсолютно нісенітниця. Реалізація функції квадратного кореня повинна суттєво відрізнятися залежно від типу, з яким вони працюють, щоб не бути жахливо неефективними.
Р ..

25

Математичні операції часто дуже чутливі до продуктивності. Тому ми хочемо використовувати статичні методи, які можуть бути повністю вирішені (і оптимізовані, або вбудовані) під час компіляції. Деякі мови не пропонують жодного механізму, який би визначав статично відправлені методи. Крім того, об'єктна модель багатьох мов має значні накладні витрати на пам'ять, що неприйнятно для "примітивних" типів, таких як double.

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

Є кілька мов, які визначають методи за їх числовими типами. Зазвичай це дуже динамічні мови, де все є об'єктом. Тут продуктивність є вторинною увагою до концептуальної елегантності, але ці мови, як правило, не використовуються для стискання чисел. Однак ці мови можуть мати оптимізатор, який може "розблокувати" операції над примітивами.


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

  • математичне позначення базується на операторах і функціях, а не на методах. Такий вираз, як 42.sqrtбагатьом користувачам, здасться набагато чужому, ніж sqrt(42). Як математично важкий користувач, я віддаю перевагу здатності створювати власні оператори над синтаксисом dot-method-call.
  • Принцип єдиної відповідальності заохочує нас обмежувати кількість операцій, що входять до типу, до основних операцій. У порівнянні з множенням, квадратний корінь потрібен надзвичайно рідко. Якщо ваш язик спеціально призначений для статистичної anlysis, а потім забезпечити більш примітивів (наприклад, операції mean, median, variance, std, normalizeчислові списки, або гамма - функції для чисел) може бути корисним. Для мови загального призначення це просто зважує інтерфейс. Віднесення несуттєвих операцій до окремого простору імен робить тип більш доступним для більшості користувачів.

Python - хороший приклад мови "все є об'єкт", що використовується для сильного стискання чисел. Скалярі NumPy насправді мають десятки і десятки методів, але sqrt все ще не є одним з них. Більшість з них є такими речами , як transposeі meanщо тільки там , щоб забезпечити єдиний інтерфейс з NumPy масивами, які є структурою даних в реальному конячці.
user2357112 підтримує Моніку

7
@ user2357112: Справа в тому, що сам NumPy написаний у суміші C і Cython, з деяким клеєм Python. Інакше вона ніколи не може бути такою швидкою, як є.
Кевін

1
Я думаю, що ця відповідь досить тісно підходить до якихось компромісів у реальному світі, досягнутих за роки дизайну. В інших новинах, чи дійсно має сенс у .Net бути в змозі зробити "Hello World" .Max (), оскільки розширення LINQ дозволяє нам і робить дуже помітним в Intellisense. Бонусні бали: Який результат? Бонусний бонус, який результат у Unicode ...?
Ендіз Сміт

15

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

Інша практична причина полягає в тому, що, виявляється, можуть бути різні способи впровадження чисельних методів з різною ефективністю та точністю компромісів. У Java є Math, і вона теж є StrictMath.


Я сподіваюся, що мовні дизайнери не переймаються питаннями автоматичного заповнення підказок. Що ж, що відбувається далі Math.<^space>? Ця автоматична заповнення підказок також буде забруднена. І навпаки, я думаю, що ваш другий абзац, мабуть, є однією з кращих відповідей тут.
Qix

@Qix Вони так роблять. Хоча, інші люди можуть назвати це "роблячи інтерфейс роздутим".
Олександр Дубінський

6

Ви правильно помітили, що тут грає допитлива симетрія.

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

Ось чому є певний аргумент певних дизайнерів мови, щоб зробити два синтаксиси взаємозамінними. Мова програмування D вже дозволяє це за допомогою функції під назвою Синтаксис єдиної функції виклику . Подібна особливість також була запропонована для стандартизації в C ++ . Як в коментарях зазначає Марк Амеррі , Python це дозволяє також.

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

Гадаю, лише час покаже, чи можливе об'єднання двох з часом, але це, безумовно, цікавий розгляд.


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.*;

З іншого боку, інші мови не мають такої проблеми, тому що вони не зачіпають вільних функцій (необов'язково, використовуючи простори імен) або набагато менше малих типів.


1
Розглянемо радість втілення змін Math.min()у всіх типах обгортки.

Я вважаю №4 непереконливим. Math.sqrt()був створений одночасно з рештою Java, тому коли було прийнято рішення поставити sqrt (), Mathне було історичної інерції користувачів Java, яким "так подобається". Хоча з цим не виникає особливих проблем sqrt(), поведінка з перевантаженням Math.round()жорстока. Можливість використовувати синтаксис учасника зі значеннями типу floatі doubleдозволила б уникнути цієї проблеми.
supercat

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))

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

Отже, розташування квадратного кореня зводиться до філософії / смаків мови та дизайнера бібліотеки. Деякі з них можуть вибрати , щоб покласти його «всередині» числові значення (наприклад , зробити це метод примірника), деякі з них можуть вибрати , щоб покласти його на тому ж рівні, що і примітивних операцій (це може означати метод примірника, або це може означати , що живуть за межами числові значення, але всередині одного і того ж модуля / класу / простору імен, наприклад, в якості окремої функції або статичного методу), деякі можуть вирішити помістити його в колекцію «помічницьких» функцій, а інші можуть вирішити делегувати його стороннім бібліотекам.


Ніщо не завадить мові дозволити статичний метод викликати або з використанням синтаксису члена (як у методах розширення C # або vb.net), або з варіацією послідовності членів (я б хотів, щоб бачив синтаксис з двома крапками, так щоб дозволити перевагам Intellisense можливість перелічити лише ті функції, які були придатні для основного аргументу, але уникнути неоднозначності з операторами реальних членів).
supercat

-2

У Java та C # ToString - це метод об’єкта, корінь ієрархії класів, тому кожен об’єкт реалізує метод ToString. Для цілого типу цілком природно, що реалізація ToString працювала б таким чином.

Отже, ви міркуєте, що це неправильно. Типи причин значення реалізують ToString не в тому, що деяким людям подобалося: ей, давайте будемо використовувати метод ToString для типів Value. Це тому, що ToString вже є, і це "найприродніша" річ.


1
Звичайно, це рішення, тобто рішення objectмати ToString()метод. Тобто, за вашими словами, "деяким людям подобалося: ей, давайте будемо метод ToString для типів значень".
Residuum

-3

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

Більше того, об'єкт Math містить інші статичні члени, і є більше сенсу з’єднати їх і використовувати однаково.


3
Приклад лічильника: BigInteger.pow () .
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.