Dans une discussion sur les méthodes statiques et d'instance, je pense toujours que Sqrt()
devrait être une méthode d'instance de types numériques au lieu d'une méthode statique. Pourquoi donc? Cela fonctionne évidemment sur une valeur.
// looks wrong to me
var y = Math.Sqrt(x);
// looks better to me
var y = x.Sqrt();
Les types de valeur peuvent évidemment avoir des méthodes d'instance, comme dans de nombreux langages, il existe une méthode d'instance ToString()
.
Pour répondre à certaines questions des commentaires: Pourquoi 1.Sqrt()
ne devrait-il pas être légal? 1.ToString()
est.
Certaines langues ne permettent pas d'avoir des méthodes sur les types de valeurs, mais certaines langues le peuvent. Je parle de ceux-ci, y compris Java, ECMAScript, C # et Python (avec __str__(self)
définie). La même chose s'applique à d'autres fonctions comme ceil()
, floor()
etc.
C'est entièrement un choix de conception de langage. Cela dépend également de l'implémentation sous-jacente des types primitifs et des considérations de performances qui en découlent.
.NET a juste ne méthode statique Math.Sqrt
Qui agit sur un double
et retourne un double
. Tout ce que vous lui passez doit être converti ou promu en double
.
double sqrt2 = Math.Sqrt(2d);
D'autre part, vous avez Rust which expose ces opérations en tant que fonctions sur les types :
let sqrt2 = 2.0f32.sqrt();
let higher = 2.0f32.max(3.0f32);
Mais Rust a également une syntaxe d'appel de fonction universelle (quelqu'un l'a mentionné plus tôt), vous pouvez donc choisir ce que vous voulez.
let sqrt2 = f32::sqrt(2.0f32);
let higher = f32::max(2.0f32, 3.0f32);
Supposons que nous concevons un nouveau langage et que nous voulons que Sqrt
soit une méthode d'instance. Nous examinons donc la classe double
et commençons la conception. Il n'a évidemment aucune entrée (autre que l'instance) et retourne un double
. Nous écrivons et testons le code. La perfection.
Mais prendre la racine carrée d'un entier est également valable, et nous ne voulons pas forcer tout le monde à se convertir en double juste pour prendre une racine carrée. Nous passons donc à int
et commençons à concevoir. Que revient-il? Nous pourrions retourner un int
et le faire fonctionner uniquement pour des carrés parfaits, ou arrondir le résultat au int
le plus proche (en ignorant le débat sur la méthode d'arrondi appropriée pour l'instant ). Mais que faire si quelqu'un veut un résultat non entier? Devrions-nous avoir deux méthodes - une qui retourne un int
et une qui retourne un double
(ce qui n'est pas possible dans certaines langues sans changer le nom). Nous décidons donc qu'il doit retourner un double
. Maintenant, nous mettons en œuvre. Mais l'implémentation est identique à celle que nous avons utilisée pour double
. Copions-collons-nous? Convertissons-nous l'instance en double
et appelons-nous à la méthode d'instance that? Pourquoi ne pas mettre la logique dans une méthode de bibliothèque accessible à partir des deux classes. Nous appellerons la bibliothèque Math
et la fonction Math.Sqrt
.
Pourquoi est-ce
Math.Sqrt
une fonction statique?:
Nous n'avons même pas abordé d'autres arguments:
GetSqrt
car il retourne une nouvelle valeur plutôt que de modifier l'instance?Square
? Abs
? Trunc
? Log10
? Ln
? Power
? Factorial
? Sin
? Cos
? ArcTan
?Les opérations mathématiques sont souvent très sensibles aux performances. Par conséquent, nous voudrons utiliser des méthodes statiques qui peuvent être entièrement résolues (et optimisées ou intégrées) au moment de la compilation. Certains langages n'offrent aucun mécanisme pour spécifier des méthodes distribuées statiquement. De plus, le modèle objet de nombreux langages a une surcharge de mémoire considérable qui est inacceptable pour les types "primitifs" tels que double
.
Quelques langages nous permettent de définir des fonctions qui utilisent la syntaxe d'invocation de méthode, mais sont en fait distribuées statiquement. Les méthodes d'extension en C # 3.0 ou version ultérieure en sont un exemple. Les méthodes non virtuelles (par exemple, la méthode par défaut pour les méthodes en C++) sont un autre cas, bien que C++ ne prenne pas en charge les méthodes sur les types primitifs. Vous pouvez bien sûr créer votre propre classe wrapper en C++ qui décore un type primitif avec diverses méthodes, sans surcharge d'exécution. Cependant, vous devrez convertir manuellement les valeurs dans ce type d'encapsuleur.
Il existe quelques langages qui définissent des méthodes sur leurs types numériques. Ce sont généralement des langages très dynamiques où tout est un objet. Ici, la performance est une considération secondaire à l'élégance conceptuelle, mais ces langages ne sont généralement pas utilisés pour le calcul des nombres. Cependant, ces langages peuvent avoir un optimiseur qui peut "décompresser" les opérations sur les primitives.
Avec les considérations techniques à l'écart, nous pouvons examiner si une telle interface mathématique basée sur une méthode serait une bonne interface. Deux problèmes se posent:
42.sqrt
Apparaîtra beaucoup plus étrangère à de nombreux utilisateurs que sqrt(42)
. En tant qu'utilisateur lourd en mathématiques, je préférerais plutôt la possibilité de créer mes propres opérateurs plutôt que la syntaxe d'appel par méthode point.mean
, median
, variance
, std
, normalize
sur les listes numériques, ou la fonction Gamma pour les nombres) peut être utile. Pour un langage à usage général, cela alourdit simplement l'interface. Reléguer les opérations non essentielles dans un espace de noms séparé rend le type plus accessible à la majorité des utilisateurs.Je serais motivé par le fait qu'il y a une tonne de fonctions mathématiques à usage spécial, et plutôt que de remplir chaque type mathématique avec toutes (ou un sous-ensemble aléatoire) de ces fonctions, vous les mettez dans une classe utilitaire. Sinon, vous pollueriez votre info-bulle de saisie semi-automatique ou vous forceriez les gens à toujours regarder à deux endroits. (Est-ce que sin
est suffisamment important pour être membre de Double
, ou est-ce dans la classe Math
avec des consanguins comme htan
et exp1p
?)
Une autre raison pratique est qu'il s'avère qu'il peut y avoir différentes façons de mettre en œuvre des méthodes numériques, avec des compromis de performances et de précision différents. Java a Math
, et il a également StrictMath
.
Vous avez correctement observé qu'il y a ici une curieuse symétrie.
Que je dise que sqrt(n)
ou n.sqrt()
n'a pas vraiment d'importance, ils expriment tous deux la même chose et celle que vous préférez est plus une question de goût personnel qu'autre chose.
C'est aussi la raison pour laquelle certains concepteurs de langage plaident fortement pour rendre les deux syntaxes interchangeables. Le langage de programmation D le permet déjà sous une fonctionnalité appelée Syntaxe d'appel de fonction uniforme . Une fonctionnalité similaire a également été proposée pour la normalisation en C++ . Comme Mark Amery le souligne dans les commentaires , Python le permet aussi.
Ce n'est pas sans problèmes. L'introduction d'un changement de syntaxe fondamental comme celui-ci a de vastes conséquences sur le code existant et est bien sûr également un sujet de discussions controversées parmi les développeurs qui ont été formés pendant des décennies à penser que les deux syntaxes décrivent des choses différentes.
Je suppose que seul le temps nous dira si l'unification des deux est réalisable à long terme, mais c'est certainement une considération intéressante.
En plus de la réponse de D Stanley vous devez penser au polymorphisme. Des méthodes comme Math.Sqrt doivent toujours renvoyer la même valeur à la même entrée. Rendre la méthode statique est un bon moyen de clarifier ce point, car les méthodes statiques ne sont pas remplaçables.
Vous avez mentionné la méthode ToString (). Ici, vous voudrez peut-être remplacer cette méthode, de sorte que la (sous) classe est représentée d'une autre manière que String comme sa classe parente. Vous en faites donc une méthode d'instance.
Eh bien, dans Java il y a un wrapper pour chaque type de base.
Et les types de base ne sont pas des types de classe et n'ont pas de fonctions membres.
Vous avez donc les choix suivants:
Math
.Excluons l'option 4, car ... Java est Java, et les adhérents professent l'aimer de cette façon.
Maintenant, nous pouvons également exclure l'option 3, car si l'allocation d'objets est assez bon marché, elle n'est pas gratuite, et le faire encore et encore ajoute vers le haut.
Deux vers le bas, un pour tuer: l'option 2 est également une mauvaise idée, car cela signifie que chaque fonction doit être implémentée pour tous les types , on ne peut pas compter sur l'élargissement de la conversion pour combler les lacunes, ou les incohérences seront vraiment douloureuses.
Et jetez un œil à Java.lang.Math
, il y a beaucoup d'écarts, en particulier pour les types plus petits que int
respectifs double
.
Donc, en fin de compte, le vainqueur clair est l'option un, les rassemblant tous au même endroit dans une classe de fonctions utilitaires.
En revenant à l'option 4, quelque chose dans ce sens s'est produit beaucoup plus tard: vous pouvez demander au compilateur de prendre en compte tous les membres statiques de n'importe quelle classe lorsque vous résolvez des noms depuis assez longtemps. import static someclass.*;
Soit dit en passant, les autres langages n'ont pas ce problème, soit parce qu'ils n'ont aucun préjugé contre les fonctions libres (utilisant éventuellement des espaces de noms) ou bien moins de petits types.
Un point que je ne vois pas mentionné explicitement (bien que amon y fasse allusion) est que la racine carrée peut être considérée comme une opération "dérivée": si l'implémentation ne nous la fournit pas, nous pouvons écrire la nôtre.
Étant donné que la question est étiquetée avec la conception de la langue, nous pourrions envisager une description indépendante de la langue. Bien que de nombreuses langues aient des philosophies différentes, il est très courant dans les paradigmes d'utiliser l'encapsulation pour préserver les invariants; c'est-à-dire pour éviter d'avoir une valeur qui ne se comporte pas comme le suggère son type.
Par exemple, si nous avons une implémentation d'entiers utilisant des mots machine, nous voulons probablement encapsuler la représentation d'une manière ou d'une autre (par exemple pour empêcher les décalages de bits de changer le signe), mais en même temps, nous avons toujours besoin d'accéder à ces bits pour implémenter des opérations comme une addition.
Certains langages peuvent implémenter cela avec des classes et des méthodes privées:
class Int {
public Int add(Int x) {
// Do something with the bits
}
private List<Boolean> getBits() {
// ...
}
}
Certains avec des systèmes de modules:
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
Certains à portée lexicale:
(defun getAdder ()
(let ((getBits (lambda (x) ; ...
(add (lambda (x y) ; Do something with the bits
'add))
Etc. Cependant, aucun de ces mécanismes sont nécessaires pour implémenter la racine carrée: il peut être implémenté en utilisant l'interface public de type numérique, et donc il n'a pas besoin d'accéder à la détails de mise en œuvre encapsulés.
Par conséquent, l'emplacement de la racine carrée se résume à la philosophie/aux goûts de la langue et du concepteur de la bibliothèque. Certains peuvent choisir de le mettre "à l'intérieur" des valeurs numériques (par exemple, en faire une méthode d'instance), certains peuvent choisir de le mettre au même niveau que les opérations primitives (cela peut signifier une méthode d'instance, ou cela peut signifier vivre - extérieur les valeurs numériques, mais intérieur le même module/classe/espace de noms, par exemple en tant que fonction autonome ou méthode statique), certains pourraient choisir de le mettre dans une collection de "helper" fonctions, certains pourraient choisir de le déléguer à des bibliothèques tierces.