Définition de l'opérateur «==» pour Double


126

Pour une raison quelconque, je me suis faufilé dans la source .NET Framework de la classe Doubleet j'ai découvert que la déclaration de ==est:

public static bool operator ==(Double left, Double right) {
    return left == right;
}

La même logique s'applique à chaque opérateur.


  • Quel est l'intérêt d'une telle définition?
  • Comment ça marche?
  • Pourquoi ne crée-t-il pas une récursion infinie?

17
Je m'attends à une récursion sans fin.
HimBromBeere

5
Je suis à peu près sûr qu'il n'est utilisé pour la comparaison nulle part avec double, mais ceqest plutôt publié en IL. Ceci est juste là pour remplir un but de documentation, mais je ne trouve pas la source.
Habib

2
Très probablement pour que cet opérateur puisse être obtenu par réflexion.
Damien_The_Unbeliever

3
Cela ne sera jamais appelé, le compilateur a la logique d'égalité intégrée (opcode ceq) voir Quand l'opérateur == de Double est
Alex K.

1
@ZoharPeled divisant un double par zéro est valide et se traduira par l'infini positif ou négatif.
Magnus

Réponses:


62

En réalité, le compilateur transformera l' ==opérateur en ceqcode IL et l'opérateur que vous mentionnez ne sera pas appelé.

La raison pour laquelle l'opérateur dans le code source est probable est qu'il peut être appelé à partir de langages autres que C # qui ne le traduisent pas en un CEQappel directement (ou par réflexion). Le code dans l'opérateur sera compilé en a CEQ, il n'y a donc pas de récursivité infinie.

En fait, si vous appelez l'opérateur par réflexion, vous pouvez voir que l'opérateur est appelé (plutôt qu'une CEQinstruction), et n'est évidemment pas infiniment récursif (puisque le programme se termine comme prévu):

double d1 = 1.1;
double d2 = 2.2;

MethodInfo mi = typeof(Double).GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public );

bool b = (bool)(mi.Invoke(null, new object[] {d1,d2}));

IL résultant (compilé par LinqPad 4):

IL_0000:  nop         
IL_0001:  ldc.r8      9A 99 99 99 99 99 F1 3F 
IL_000A:  stloc.0     // d1
IL_000B:  ldc.r8      9A 99 99 99 99 99 01 40 
IL_0014:  stloc.1     // d2
IL_0015:  ldtoken     System.Double
IL_001A:  call        System.Type.GetTypeFromHandle
IL_001F:  ldstr       "op_Equality"
IL_0024:  ldc.i4.s    18 
IL_0026:  call        System.Type.GetMethod
IL_002B:  stloc.2     // mi
IL_002C:  ldloc.2     // mi
IL_002D:  ldnull      
IL_002E:  ldc.i4.2    
IL_002F:  newarr      System.Object
IL_0034:  stloc.s     04 // CS$0$0000
IL_0036:  ldloc.s     04 // CS$0$0000
IL_0038:  ldc.i4.0    
IL_0039:  ldloc.0     // d1
IL_003A:  box         System.Double
IL_003F:  stelem.ref  
IL_0040:  ldloc.s     04 // CS$0$0000
IL_0042:  ldc.i4.1    
IL_0043:  ldloc.1     // d2
IL_0044:  box         System.Double
IL_0049:  stelem.ref  
IL_004A:  ldloc.s     04 // CS$0$0000
IL_004C:  callvirt    System.Reflection.MethodBase.Invoke
IL_0051:  unbox.any   System.Boolean
IL_0056:  stloc.3     // b
IL_0057:  ret 

Il est intéressant - les mêmes opérateurs n'existent pas ( que ce soit dans la source de référence ou par réflexion) pour les types entiers, seulement Single, Double, Decimal, Stringet DateTimequi réfute ma théorie selon laquelle ils existent pour appeler d'autres langues. Évidemment, vous pouvez assimiler deux entiers dans d'autres langues sans ces opérateurs, nous revenons donc à la question "pourquoi existent-ils pour double"?


12
Le seul problème que je peux voir avec cela est que la spécification du langage C # dit que les opérateurs surchargés ont la priorité sur les opérateurs intégrés. Alors sûrement, un compilateur C # conforme devrait voir qu'un opérateur surchargé est disponible ici et générer la récursivité infinie. Hmm. Troublant.
Damien_The_Unbeliever

5
Cela ne répond pas à la question, à mon humble avis. Il explique seulement en quoi le code est traduit mais pas pourquoi. Selon la section 7.3.4 Résolution de surcharge d'opérateur binaire de la spécification du langage C #, je m'attendrais également à une récursivité infinie. Je suppose que la source de référence ( referencesource.microsoft.com/#mscorlib/system/… ) ne s'applique pas vraiment ici.
Dirk Vollmar le

6
@DStanley - Je ne nie pas ce qui est produit. Je dis que je ne peux pas le concilier avec la spécification de la langue. Voilà ce qui est troublant. Je pensais à parcourir Roslyn et à voir si je pouvais trouver une manipulation spéciale ici, mais je ne suis pas bien préparé pour le faire pour le moment (mauvaise machine)
Damien_The_Unbeliever

1
@Damien_The_Unbeliever C'est pourquoi je pense que c'est soit une exception à la spécification, soit une interprétation différente des opérateurs "intégrés".
D Stanley

1
Comme @Jon Skeet n'a pas encore répondu ou commenté cela, je soupçonne que c'est un bug (c'est-à-dire une violation des spécifications).
TheBlastOne

37

La principale confusion ici est que vous supposez que toutes les bibliothèques .NET (dans ce cas, la bibliothèque numérique étendue, qui ne fait pas partie de la BCL) sont écrites en C # standard. Ce n'est pas toujours le cas et différentes langues ont des règles différentes.

En C # standard, le morceau de code que vous voyez entraînerait un débordement de pile, en raison du fonctionnement de la résolution de surcharge des opérateurs. Cependant, le code n'est pas réellement en C # standard - il utilise essentiellement des fonctionnalités non documentées du compilateur C #. Au lieu d'appeler l'opérateur, il émet ce code:

ldarg.0
ldarg.1
ceq
ret

Voilà :) Il n'y a pas de code C # 100% équivalent - ce n'est tout simplement pas possible en C # avec votre propre type.

Même dans ce cas, l'opérateur réel n'est pas utilisé lors de la compilation de code C # - le compilateur fait un tas d'optimisations, comme dans ce cas, où il remplace l' op_Equalityappel par le simple ceq. Encore une fois, vous ne pouvez pas répliquer cela dans votre propre DoubleExstructure - c'est la magie du compilateur.

Ce n'est certainement pas une situation unique dans .NET - il y a beaucoup de code qui n'est pas valide, C # standard. Les raisons sont généralement (a) des hacks de compilateur et (b) un langage différent, avec les hacks d'exécution impairs (c) (je vous regarde Nullable,!).

Étant donné que le compilateur Roslyn C # est une source oepn, je peux en fait vous indiquer l'endroit où la résolution de surcharge est décidée:

L'endroit où tous les opérateurs binaires sont résolus

Les "raccourcis" pour les opérateurs intrinsèques

Lorsque vous regardez les raccourcis, vous verrez que l'égalité entre double et double se traduit par l'opérateur double intrinsèque, jamais par l' ==opérateur réel défini sur le type. Le système de types .NET doit prétendre que Doublec'est un type comme les autres, mais pas C # - doubleest une primitive en C #.


1
Je ne suis pas sûr d’accepter que le code de la source de référence soit simplement «rétro-ingénierie». Le code a des directives de compilateur #ifet d'autres artefacts qui ne seraient pas présents dans le code compilé. De plus, s'il a été rétro-conçu pour, doublepourquoi n'a-t-il pas été conçu pour intou long? Je pense qu'il y a une raison pour le code source, mais je crois que l'utilisation de ==à l'intérieur de l'opérateur est compilée en un CEQqui empêche la récursivité. Puisque l'opérateur est un opérateur "prédéfini" pour ce type (et ne peut pas être remplacé), les règles de surcharge ne s'appliquent pas.
D Stanley

@DStanley Je ne voulais pas laisser entendre que tout le code est rétro-conçu. Et encore une fois, doublene fait pas partie de la BCL - c'est dans une bibliothèque séparée, qui se trouve juste être incluse dans la spécification C #. Oui, le ==est compilé en a ceq, mais cela signifie toujours qu'il s'agit d'un hack de compilateur que vous ne pouvez pas répliquer dans votre propre code, et quelque chose qui ne fait pas partie de la spécification C # (tout comme le float64champ de la Doublestructure). Ce n'est pas une partie contractuelle de C #, il est donc inutile de le traiter comme un C # valide, même s'il a été compilé avec le compilateur C #.
Luaan

@DStanely Je n'ai pas pu trouver comment le cadre réel est organisé, mais dans l'implémentation de référence de .NET 2.0, toutes les parties délicates ne sont que des éléments intrinsèques du compilateur, implémentés en C ++. Il y a encore beaucoup de code natif .NET, bien sûr, mais des choses comme "comparer deux doubles" ne fonctionneraient pas vraiment bien en .NET pur; c'est l'une des raisons pour lesquelles les nombres à virgule flottante ne sont pas inclus dans la BCL. Cela dit, le code est également implémenté en C # (non standard), probablement exactement pour la raison que vous avez mentionnée précédemment - pour vous assurer que les autres compilateurs .NET peuvent traiter ces types comme de vrais types .NET.
Luaan le

@DStanley Mais d'accord, point pris. J'ai supprimé la référence «reverse engineering» et reformulé la réponse pour mentionner explicitement «C # standard», plutôt que juste C #. Et ne traitez pas de doublela même manière que intet long- intet ce longsont des types primitifs que tous les langages .NET doivent prendre en charge. float, decimalet doublene le sont pas.
Luaan

12

La source des types primitifs peut prêter à confusion. Avez-vous vu la toute première ligne de la Doublestructure?

Normalement, vous ne pouvez pas définir une structure récursive comme celle-ci:

public struct Double : IComparable, IFormattable, IConvertible
        , IComparable<Double>, IEquatable<Double>
{
    internal double m_value; // Self-recursion with endless loop?
    // ...
}

Les types primitifs ont également leur support natif dans CIL. Normalement, ils ne sont pas traités comme des types orientés objet. Un double est juste une valeur 64 bits s'il est utilisé comme float64dans CIL. Cependant, s'il est traité comme un type .NET habituel, il contient une valeur réelle et il contient des méthodes comme tous les autres types.

Donc ce que vous voyez ici est la même situation pour les opérateurs. Normalement, si vous utilisez directement le type double, il ne sera jamais appelé. BTW, sa source ressemble à ceci dans CIL:

.method public hidebysig specialname static bool op_Equality(float64 left, float64 right) cil managed
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor()
    .custom instance void __DynamicallyInvokableAttribute::.ctor()
    .maxstack 8
    L_0000: ldarg.0
    L_0001: ldarg.1
    L_0002: ceq
    L_0004: ret
}

Comme vous pouvez le voir, il n'y a pas de boucle sans fin (l' ceqinstrument est utilisé au lieu d'appeler le System.Double::op_Equality). Ainsi, lorsqu'un double est traité comme un objet, la méthode opérateur sera appelée, qui finira par le traiter comme le float64type primitif au niveau CIL.


1
Pour ceux qui ne comprennent pas la première partie de cet article (peut-être parce qu'ils n'écrivent généralement pas leurs propres types de valeur), essayez le code public struct MyNumber { internal MyNumber m_value; }. Il ne peut pas être compilé, bien sûr. L'erreur est l' erreur CS0523: Le membre de structure 'MyNumber.m_value' de type 'MyNumber' provoque un cycle dans la mise en page de structure
Jeppe Stig Nielsen

8

J'ai jeté un œil au CIL avec JustDecompile. L'intérieur ==est traduit en code d' opération CIL ceq . En d'autres termes, c'est l'égalité CLR primitive.

J'étais curieux de voir si le compilateur C # ferait référence ceqou l' ==opérateur lors de la comparaison de deux valeurs doubles. Dans l'exemple trivial que j'ai trouvé (ci-dessous), il a utilisé ceq.

Ce programme:

void Main()
{
    double x = 1;
    double y = 2;

    if (x == y)
        Console.WriteLine("Something bad happened!");
    else
        Console.WriteLine("All is right with the world");
}

génère le CIL suivant (notez l'instruction avec l'étiquette IL_0017):

IL_0000:  nop
IL_0001:  ldc.r8      00 00 00 00 00 00 F0 3F
IL_000A:  stloc.0     // x
IL_000B:  ldc.r8      00 00 00 00 00 00 00 40
IL_0014:  stloc.1     // y
IL_0015:  ldloc.0     // x
IL_0016:  ldloc.1     // y
IL_0017:  ceq
IL_0019:  stloc.2
IL_001A:  ldloc.2
IL_001B:  brfalse.s   IL_002A
IL_001D:  ldstr       "Something bad happened!"
IL_0022:  call        System.Console.WriteLine
IL_0027:  nop
IL_0028:  br.s        IL_0035
IL_002A:  ldstr       "All is right with the world"
IL_002F:  call        System.Console.WriteLine
IL_0034:  nop
IL_0035:  ret

-2

Comme indiqué dans la documentation Microsoft pour l'espace de noms System.Runtime.Versioning: les types trouvés dans cet espace de noms sont destinés à être utilisés dans le .NET Framework et non pour les applications utilisateur. L'espace de noms System.Runtime.Versioning contient des types avancés qui prennent en charge la gestion des versions dans implémentations côte à côte du .NET Framework.


Qu'est-ce qui a System.Runtime.Versioningà voir avec System.Double?
Koopakiller
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.