web-dev-qa-db-fra.com

Unités de mesure en C # - presque

Inspiré par nités de mesure en F # , et malgré l'affirmation ( ici ) que vous ne pouviez pas le faire en C #, j'ai eu une idée l'autre jour que j'ai été jouer avec.

namespace UnitsOfMeasure
{
    public interface IUnit { }
    public static class Length
    {
        public interface ILength : IUnit { }
        public class m : ILength { }
        public class mm : ILength { }
        public class ft : ILength { }
    }
    public class Mass
    {
        public interface IMass : IUnit { }
        public class kg : IMass { }
        public class g : IMass { }
        public class lb : IMass { }
    }

    public class UnitDouble<T> where T : IUnit
    {
        public readonly double Value;
        public UnitDouble(double value)
        {
            Value = value;
        }
        public static UnitDouble<T> operator +(UnitDouble<T> first, UnitDouble<T> second)
        {
            return new UnitDouble<T>(first.Value + second.Value);
        }
        //TODO: minus operator/equality
    }
}

Exemple d'utilisation:

var a = new UnitDouble<Length.m>(3.1);
var b = new UnitDouble<Length.m>(4.9);
var d = new UnitDouble<Mass.kg>(3.4);
Console.WriteLine((a + b).Value);
//Console.WriteLine((a + c).Value); <-- Compiler says no

L'étape suivante tente d'implémenter des conversions (extrait):

public interface IUnit { double toBase { get; } }
public static class Length
{
    public interface ILength : IUnit { }
    public class m : ILength { public double toBase { get { return 1.0;} } }
    public class mm : ILength { public double toBase { get { return 1000.0; } } }
    public class ft : ILength { public double toBase { get { return 0.3048; } } }
    public static UnitDouble<R> Convert<T, R>(UnitDouble<T> input) where T : ILength, new() where R : ILength, new()
    {
        double mult = (new T() as IUnit).toBase;
        double div = (new R() as IUnit).toBase;
        return new UnitDouble<R>(input.Value * mult / div);
    }
}

(J'aurais aimé éviter d'instancier des objets en utilisant statique, mais comme nous le savons tous ne peut pas déclarer une méthode statique dans une interface ) Vous pouvez alors faire ceci:

var e = Length.Convert<Length.mm, Length.m>(c);
var f = Length.Convert<Length.mm, Mass.kg>(d); <-- but not this

De toute évidence, il y a un trou béant dans cela, par rapport aux unités de mesure F # (je vous laisse le déterminer).

Oh, la question est: qu'en pensez-vous? Vaut-il la peine? Quelqu'un d'autre a-t-il déjà fait mieux?

[~ # ~] mise à jour [~ # ~] pour les personnes intéressées par ce sujet, ici est un lien vers un article de 1997 discutant d'un autre type de solution (pas spécifiquement pour C #)

57
Benjol

Il vous manque une analyse dimensionnelle. Par exemple (à partir de la réponse à laquelle vous avez lié), en F #, vous pouvez faire ceci:

let g = 9.8<m/s^2>

et il générera une nouvelle unité d'accélération, dérivée de mètres et de secondes (vous pouvez réellement faire la même chose en C++ en utilisant des modèles).

En C #, il est possible de faire une analyse dimensionnelle au moment de l'exécution, mais cela ajoute des frais généraux et ne vous donne pas l'avantage de la vérification au moment de la compilation. Pour autant que je sache, il n'y a aucun moyen de faire des unités de compilation complètes en C #.

Que cela en vaille la peine dépend de l'application, bien sûr, mais pour de nombreuses applications scientifiques, c'est certainement une bonne idée. Je ne connais aucune bibliothèque existante pour .NET, mais elles existent probablement.

Si vous souhaitez savoir comment le faire lors de l'exécution, l'idée est que chaque valeur a une valeur scalaire et des entiers représentant la puissance de chaque unité de base.

class Unit
{
    double scalar;
    int kg;
    int m;
    int s;
    // ... for each basic unit

    public Unit(double scalar, int kg, int m, int s)
    {
       this.scalar = scalar;
       this.kg = kg;
       this.m = m;
       this.s = s;
       ...
    }

    // For addition/subtraction, exponents must match
    public static Unit operator +(Unit first, Unit second)
    {
        if (UnitsAreCompatible(first, second))
        {
            return new Unit(
                first.scalar + second.scalar,
                first.kg,
                first.m,
                first.s,
                ...
            );
        }
        else
        {
            throw new Exception("Units must match for addition");
        }
    }

    // For multiplication/division, add/subtract the exponents
    public static Unit operator *(Unit first, Unit second)
    {
        return new Unit(
            first.scalar * second.scalar,
            first.kg + second.kg,
            first.m + second.m,
            first.s + second.s,
            ...
        );
    }

    public static bool UnitsAreCompatible(Unit first, Unit second)
    {
        return
            first.kg == second.kg &&
            first.m == second.m &&
            first.s == second.s
            ...;
    }
}

Si vous ne permettez pas à l'utilisateur de modifier la valeur des unités (une bonne idée de toute façon), vous pouvez ajouter des sous-classes pour les unités communes:

class Speed : Unit
{
    public Speed(double x) : base(x, 0, 1, -1, ...); // m/s => m^1 * s^-1
    {
    }
}

class Acceleration : Unit
{
    public Acceleration(double x) : base(x, 0, 1, -2, ...); // m/s^2 => m^1 * s^-2
    {
    }
}

Vous pouvez également définir des opérateurs plus spécifiques sur les types dérivés pour éviter de rechercher des unités compatibles sur les types courants.

41
Matthew Crumley

Vous pouvez ajouter des méthodes d'extension aux types numériques pour générer des mesures. Cela ressemblerait un peu à DSL:

var mass = 1.Kilogram();
var length = (1.2).Kilometres();

Ce n'est pas vraiment une convention .NET et ce n'est peut-être pas la fonctionnalité la plus découvrable, alors vous pouvez peut-être les ajouter dans un espace de noms dédié pour les gens qui les aiment, ainsi que proposer des méthodes de construction plus conventionnelles.

18
Drew Noakes

L'utilisation de classes séparées pour différentes unités de la même mesure (par exemple, cm, mm et ft pour la longueur) semble assez étrange. Sur la base des classes DateTime et TimeSpan du .NET Framework, je m'attendrais à quelque chose comme ceci:

Length  length       = Length.FromMillimeters(n1);
decimal lengthInFeet = length.Feet;
Length  length2      = length.AddFeet(n2);
Length  length3      = length + Length.FromMeters(n3);
17
Mark Cidade

J'ai récemment publié Units.NET sur GitHub et sur NuGet .

Il vous donne toutes les unités et conversions courantes. Il est léger, testé à l'unité et prend en charge PCL.

Exemples de conversions:

Length meter = Length.FromMeters(1);
double cm = meter.Centimeters; // 100
double yards = meter.Yards; // 1.09361
double feet = meter.Feet; // 3.28084
double inches = meter.Inches; // 39.3701
10
angularsen

Une telle bibliothèque C # existe désormais: http://www.codeproject.com/Articles/413750/Units-of-Measure-Validator-for-Csharp

Il a presque les mêmes caractéristiques que la validation du temps de compilation des unités de F #, mais pour C #. Le cœur est une tâche MSBuild, qui analyse le code et recherche des validations.

Les informations sur l'unité sont stockées dans des commentaires et des attributs.

10
Henning

Voici mon souci de créer des unités en C #/VB. Veuillez me corriger si vous pensez que je me trompe. La plupart des implémentations que j'ai lues semblent impliquer la création d'une structure qui associe une valeur (int ou double) à une unité. Ensuite, vous essayez de définir des fonctions de base (+ - * /, etc.) pour ces structures qui prennent en compte les conversions d'unités et la cohérence.

Je trouve l'idée très attrayante, mais chaque fois que je rechigne à quelle étape énorme pour un projet cela semble être. Cela ressemble à un accord tout ou rien. Vous ne changeriez probablement pas simplement quelques chiffres en unités; le fait est que toutes les données d'un projet sont correctement étiquetées avec une unité pour éviter toute ambiguïté. Cela signifie dire au revoir à l'utilisation des doubles et des entiers ordinaires, chaque variable est maintenant définie comme une "unité" ou une "longueur" ou des "mètres", etc. Les gens le font-ils vraiment à grande échelle? Ainsi, même si vous disposez d'un grand tableau, chaque élément doit être marqué d'une unité. Cela aura évidemment des ramifications de taille et de performances.

Malgré toute l'habileté à essayer de pousser la logique de l'unité en arrière-plan, une notation lourde semble inévitable avec C #. F # fait de la magie dans les coulisses qui réduit mieux le facteur de gêne de la logique de l'unité.

En outre, comment pouvons-nous réussir à faire en sorte que le compilateur traite une unité comme un double ordinaire quand nous le souhaitons, sans utiliser CType ou ".Value" ou toute autre notation supplémentaire? Comme avec nullables, le code sait traiter un double? comme un double (bien sûr, si votre double? est nul, vous obtenez une erreur).

4
Scott

Merci pour l'idée. J'ai implémenté des unités en C # de différentes manières, il semble toujours y avoir un problème. Maintenant, je peux essayer une fois de plus en utilisant les idées discutées ci-dessus. Mon objectif est de pouvoir définir de nouvelles unités basées sur celles existantes comme

Unit lbf = 4.44822162*N;
Unit fps = feet/sec;
Unit hp = 550*lbf*fps

et pour que le programme détermine les dimensions, l'échelle et le symbole appropriés à utiliser. En fin de compte, je dois construire un système d'algèbre de base qui peut convertir des choses comme (m/s)*(m*s)=m^2 et essayez d'exprimer le résultat en fonction des unités existantes définies.

Une exigence doit également être de pouvoir sérialiser les unités de manière à ce que les nouvelles unités n'aient pas besoin d'être codées, mais simplement déclarées dans un fichier XML comme celui-ci:

<DefinedUnits>
  <DirectUnits>
<!-- Base Units -->
<DirectUnit Symbol="kg"  Scale="1" Dims="(1,0,0,0,0)" />
<DirectUnit Symbol="m"   Scale="1" Dims="(0,1,0,0,0)" />
<DirectUnit Symbol="s"   Scale="1" Dims="(0,0,1,0,0)" />
...
<!-- Derived Units -->
<DirectUnit Symbol="N"   Scale="1" Dims="(1,1,-2,0,0)" />
<DirectUnit Symbol="R"   Scale="1.8" Dims="(0,0,0,0,1)" />
...
  </DirectUnits>
  <IndirectUnits>
<!-- Composite Units -->
<IndirectUnit Symbol="m/s"  Scale="1"     Lhs="m" Op="Divide" Rhs="s"/>
<IndirectUnit Symbol="km/h" Scale="1"     Lhs="km" Op="Divide" Rhs="hr"/>
...
<IndirectUnit Symbol="hp"   Scale="550.0" Lhs="lbf" Op="Multiply" Rhs="fps"/>
  </IndirectUnits>
</DefinedUnits>
3
ja72

vous pouvez utiliser QuantitySystem au lieu de l'implémenter par vous-même. Il s'appuie sur F # et améliore considérablement la gestion des unités en F #. C'est la meilleure implémentation que j'ai trouvée jusqu'à présent et peut être utilisée dans des projets C #.

http://quantitysystem.codeplex.com

1
MovGP0

il y a jscience: http://jscience.org/ , et voici un dsl groovy pour les unités: http://groovy.dzone.com/news/domain-specific-language -unit - . iirc, c # a des fermetures, vous devriez donc être en mesure de bricoler quelque chose.

1
Ray Tayek

J'ai vraiment aimé lire cette question de débordement de pile et ses réponses.

J'ai un projet pour animaux de compagnie avec lequel j'ai bricolé au fil des ans, et j'ai récemment commencé à le réécrire et à le publier sur l'open source à http://ngenericdimensions.codeplex.com

Il se trouve qu'il est quelque peu similaire à bon nombre des idées exprimées dans les questions et réponses de cette page.

Il s'agit essentiellement de créer des dimensions génériques, avec l'unité de mesure et le type de données natif comme espaces réservés de type générique.

Par exemple:

Dim myLength1 as New Length(of Miles, Int16)(123)

Avec également une utilisation facultative des méthodes d'extension comme:

Dim myLength2 = 123.miles

Et

Dim myLength3 = myLength1 + myLength2
Dim myArea1 = myLength1 * myLength2

Cela ne compilerait pas:

Dim myValue = 123.miles + 234.kilograms

De nouvelles unités peuvent être étendues dans vos propres bibliothèques.

Ces types de données sont des structures qui ne contiennent qu'une seule variable membre interne, ce qui les rend légères.

Fondamentalement, les surcharges de l'opérateur sont limitées aux structures de "dimension", de sorte que chaque unité de mesure n'a pas besoin de surcharges de l'opérateur.

Bien sûr, un gros inconvénient est la déclaration plus longue de la syntaxe générique qui nécessite 3 types de données. Donc, si cela vous pose problème, ce n'est pas votre bibliothèque.

Le but principal était de pouvoir décorer une interface avec des unités dans un mode de vérification au moment de la compilation.

Il y a beaucoup à faire à la bibliothèque, mais je voulais le poster au cas où ce serait le genre de chose que quelqu'un recherchait.

0
Mafu Josh

Voir Boo Ometa (qui sera disponible pour Boo 1.0): Boo Ometa et Extensible Parsing

0
justin.m.chase

Pourquoi ne pas utiliser CodeDom pour générer automatiquement toutes les permutations possibles des unités? Je sais que ce n'est pas le meilleur - mais je vais certainement travailler!

0