Comment concevoir une méthode TryParse qui fournit des informations détaillées en cas d'erreur d'analyse?

9

Lors de l'analyse des entrées utilisateur, il est généralement recommandé de ne pas lever et intercepter les exceptions mais plutôt d'utiliser des méthodes de validation. Dans le .NET BCL, ce serait la différence entre, par exemple, int.Parse(lève une exception sur les données non valides) et int.TryParse(renvoie falsesur les données non valides).

Je conçois le mien

Foo.TryParse(string s, out Foo result)

et je ne suis pas sûr de la valeur de retour. Je pourrais utiliser boolcomme la propre TryParseméthode de .NET , mais cela ne donnerait aucune indication sur le type d'erreur, sur la raison exacte pour laquelle il s n'a pas pu être analysé dans un Foo. (Par exemple, spourrait avoir des parenthèses sans correspondance, ou le mauvais nombre de caractères, ou un Barsans correspondant Baz, etc.)

En tant qu'utilisateur d'API, je n'aime pas du tout les méthodes qui renvoient simplement un succès / échec booléen sans me dire pourquoi l'opération a échoué. Cela fait du débogage un jeu de devinettes, et je ne veux pas non plus imposer cela aux clients de ma bibliothèque.

Je peux penser à de nombreuses solutions de contournement à ce problème (renvoyer les codes d'état, renvoyer une chaîne d'erreur, ajouter une chaîne d'erreur comme paramètre de sortie), mais ils ont tous leurs inconvénients respectifs, et je veux également rester cohérent avec les conventions de le .NET Framework .

Ainsi, ma question est la suivante:

Existe-t-il des méthodes dans le .NET Framework qui (a) analysent les entrées sans lever d'exceptions et (b) renvoient toujours des informations d'erreur plus détaillées qu'un simple booléen vrai / faux?

Heinzi
la source
1
Ce lien ne conclut pas qu'il n'est pas recommandé de lever et d'intercepter des exceptions. Il y a des moments que la meilleure façon est d'utiliser Parse().
paparazzo

Réponses:

5

Je recommanderais d'utiliser le modèle de monade pour votre type de retour.

ParseResult<Foo> foo = FooParser.Parse("input");

Notez également qu'il ne devrait pas être de la responsabilité de Foo de comprendre comment il doit être analysé à partir des entrées utilisateur, car cela lie directement votre couche de domaine à votre couche d'interface utilisateur et viole également le principal de responsabilité unique.

Vous pouvez également créer une classe de résultats d'analyse spécifique à Fooau lieu d'utiliser des génériques, selon votre cas d'utilisation.

Une classe de résultat d'analyse spécifique à foo pourrait ressembler à ceci:

class FooParseResult
{
     Foo Value { get; set; }
     bool PassedRequirement1 { get; set; }
     bool PassedRequirement2 { get; set; }
}

Voici la version Monad:

class ParseResult<T>
{
     T Value { get; set; }
     string ParseErrorMessage { get; set; }
     bool WasSuccessful { get; set; }
}

Je ne connais aucune méthode dans le framework .net qui renvoie des informations détaillées sur les erreurs d'analyse.

TheCatWhisperer
la source
Je comprends votre commentaire sur la liaison de la couche d'interface utilisateur, mais dans ce cas, il existe une représentation de chaîne canonique standardisée de Foo, il est donc logique d'avoir Foo.ToStringet Foo.Parse.
Heinzi
Et, par ma question en gras, pouvez-vous me donner un exemple du .NET BCL qui utilise ce modèle?
Heinzi
4
Comment est-ce une monade?
JacquesB
@Heinzi: Toute méthode qui renvoie un Func<T>répondrait à ces critères, si vous incluez Tles informations dont vous avez besoin. Le retour d'informations détaillées sur les erreurs vous appartient en grande partie. Avez-vous pensé à utiliser un Maybe<T>? Voir mikhail.io/2016/01/monads-explained-in-csharp
Robert Harvey
@JacquesB: Je me demandais un peu la même chose. La signature de la méthode est compatible avec le comportement modanique, mais c'est tout.
Robert Harvey
1

Vous pouvez regarder ModelState dans le framework MVC. Il représente une tentative d'analyse de certaines entrées et peut contenir une collection d'erreurs.

Cela dit, je ne pense pas qu'il existe un modèle récurrent dans le BCL .net, car les exceptions sont - pour le meilleur ou pour le pire - le modèle établi pour signaler les conditions d'erreur dans le .net. Je pense que vous devriez simplement aller de l'avant et implémenter votre propre solution adaptée à votre problème, par exemple une ParseResultclasse avec deux sous-classes, SuccessfulParseet FailedParse, où SuccessfulParsea une propriété avec la valeur analysée et FailedParsea une propriété de message d'erreur. Combiner cela avec la correspondance de motifs en C # 7 pourrait être assez élégant.

JacquesB
la source
1

J'ai rencontré des problèmes similaires en voulant utiliser une TryParse/Convert/etc.méthode où j'ai parfois besoin de savoir comment et pourquoi elle a échoué.

J'ai fini par m'inspirer de la façon dont certains sérialiseurs gèrent les erreurs et utilisent les événements. De cette façon, la syntaxe de ma TryX(..., out T)méthode semble aussi propre que n'importe quelle autre et renvoie de manière fiable un simple falsecomme le modèle l'indique.

Cependant, lorsque je veux avoir plus de détails, j'ajoute simplement un gestionnaire d'événements et j'obtiens les résultats dont j'ai besoin dans un package aussi complexe ou simple que je le souhaite ( MyEventArgsci - dessous). Ajoutez-le à une liste de chaînes, ajoutez une ExceptionDispatchInfoet capturez les exceptions; laissez l'appelant décider si et comment il veut faire face à tout ce qui ne va pas.

public class Program
{
    public static void Main()
    {
        var c = new MyConverter();

        //here's where I'm subscibing to errors that occur
        c.Error += (sender, args) => Console.WriteLine(args.Details);

        c.TryCast<int>("5", out int i);
    }
}

//here's our converter class
public class MyConverter
{
    //invoke this event whenever something goes wrong and fill out your EventArgs with details
    public event EventHandler<MyEventArgs> Error;

    //intentionally stupid implementation
    public bool TryCast<T>(object input, out T output)
    {
        bool success = true;
        output = default (T);

        //try-catch here because it's an easy way to demonstrate my example
        try
        {
            output = (T)input;
        }
        catch (Exception ex)
        {
            success = false;
            Error?.Invoke(this, new MyEventArgs{Details = ex.ToString()});
        }

        return success;
    }
}

//stores whatever information you want to make available
public class MyEventArgs : EventArgs
{
    public string Details {get; set;}
}
Chakrava
la source