Rappresentazione di diversi risultati del metodo tramite il tipo restituito in C #

1

Sto osservando un problema in cui un metodo può fallire in modo prevedibile. Per ora penso che le eccezioni potrebbero non essere un buon modo per rappresentare alcuni dei fallimenti (potrei sbagliarmi su questo). In ogni caso stavo pensando di restituire un tipo di risultato simile a quelli in ASP.NET Identity e quelli discussi in questa domanda - Qual è un buon progetto per un metodo che può restituire diversi risultati logicamente diversi? (es. una proprietà bool o enum Risultato e altre proprietà presenti se il risultato è positivo). Comunque dal momento che il C # 7 è stato rilasciato e ha un pattern matching mi chiedo se questa non sia una soluzione migliore

public abstract class ReadFileResult
{
}

public class FileNotFoundResult : ReadFileResult
{
}

public class BytesFileResult : ReadFileResult
{
    public BytesFileResult(byte[] fileData)
    {
        FileData = fileData;
    }

    public byte[] FileData { get; }
}

class Program
{
    static void Main(string[] args)
    {
        ReadFileResult readFileResult = ReadFile("test.txt");

        switch (readFileResult)
        {
            case FileNotFoundResult fileNotFound:
                {
                    Console.WriteLine("File not found");
                }
                break;
            case BytesFileResult bytesFileResult:
                {
                    Console.WriteLine(bytesFileResult.FileData.Length); //do something with the data
                }
                break;
            default:
                {
                    throw new Exception("Unexpected file result");
                }
        }
    }

    public static ReadFileResult ReadFile(string filename)
    {
        throw new NotImplementedException(); //some logic here
    }
}

Preferiresti vedere il tipo di risultato "standard" tipo + proprietà o la modalità di corrispondenza del modello è più chiara? Se preferisci uno sull'altro perché? È possibile migliorare la soluzione basata sul pattern matching?

Modifica: solo per la cronologia questo esempio NON è il mio caso d'uso. Il mio caso d'uso specifico avrà bisogno di almeno 3 blocchi catch se vado per le eccezioni (che potrei ancora fare). Mi chino a restituire un risultato perché preferisco se oltre 3 blocchi di cattura anche se probabilmente andrei a fare un'eccezione se fosse solo un caso strano. In ogni caso la mia domanda è intesa a confrontare i due modi per restituire un risultato, non a discutere delle eccezioni rispetto ai risultati. Supponiamo che parli di ASP.NET Identity. Sarebbe meglio se invece di questa classe avevano un sacco di classi derivate e usato la corrispondenza dei pattern (supponendo che tutti i loro client usassero C # 7.0)

    
posta Stilgar 24.07.2017 - 11:00
fonte

5 risposte

4

Direi che questo è letteralmente il classico caso d'uso per le eccezioni. Le eccezioni sono già dotate di un meccanismo di corrispondenza del modello perfettamente valido. Per dirla in parole povere, non c'è alcun vantaggio oltre il fatto che l'eccezione si propaghi verso l'alto e la prenda nel Main (). Ecco come sarà la tua implementazione di ReadFileResult:

public static ReadFileResult ReadFile(string filename)
{
  try {
     return new BytesFileResult(File.ReadAllBytes(filename));
  }
  catch (FileNotFoundException) {
     return new FileNotFoundResult();
  }
}

Potresti farlo per lo stesso effetto, il che ti farebbe risparmiare un sacco di inutili complessità:

class Program
{
    static void Main(string[] args)
    {
        try {
            byte[] readFileResult = ReadFile("test.txt");
            Console.WriteLine(bytesFileResult.Length); 
        }
        catch (FileNotFoundException) {
            Console.WriteLine("File not found");
        }
    }

    public static ReadFileResult ReadFile(string filename)
    {
        File.ReadAllBytes(filename); //some logic here
    }
}

Non dimenticare che ci sono altre IOException che possono accadere quando leggi un file. Vuoi creare ReadFileResults anche per quelli? Questo è solo aggiungere un ulteriore livello di riferimento indiretto senza alcun beneficio evidente.

ASP.NET utilizza il modello a più risultati in quanto è possibile restituire diversi tipi di risultati, non solo contenuti o errori. È possibile restituire una vista, JSON, un reindirizzamento, contenuto non elaborato, vuoto, così via. Questa non è la tua situazione.

Il pattern "risultato dell'errore" di per sé è utile quando devi serializzare il risultato, per esempio, e passarlo attraverso un livello che non supporta le eccezioni.

    
risposta data 24.07.2017 - 11:50
fonte
3

Hai scelto una lingua, C #, che utilizza eccezioni ovunque, e il tuo caso d'uso sembra essere esattamente ciò che le eccezioni dovrebbero essere utilizzate.

Potresti percepire qualche altro meccanismo per la gestione degli errori (ad esempio la corrispondenza dei modelli sui tipi di risultato come ad esempio in Rust o Haskell) presenta alcuni vantaggi, ma questa è davvero una scelta fatta a livello di lingua. Anche se decidi di utilizzare un meccanismo diverso, il BCL e tutte le librerie di terze parti continueranno a utilizzare le eccezioni, il che significa che dovrai supportare entrambi tipi di gestione degli errori nella tua applicazione allora, e stai andando confondere altri sviluppatori per l'avvio. Semplicemente non c'è una buona ragione per farlo.

La corrispondenza del modello sui tipi di risultato è un modello utile, ma non dovrebbe essere utilizzata come alternativa alle eccezioni.

    
risposta data 24.07.2017 - 13:01
fonte
1

Le eccezioni dovrebbero essere utilizzate solo quando la funzione ha come risultato un errore. E, se una funzione fallisce, dovrebbe restituire un'eccezione. I codici di ritorno degli errori sono accettabili solo quando le prestazioni sono di importanza assoluta o i validatori di input come Int32.TryParse.

Se una funzione ha bisogno di restituire diversi tipi di risultati "positivi", allora il pattern-matching è in realtà un buon approccio.

    
risposta data 24.07.2017 - 14:59
fonte
1

Stai chiedendo se il tipo di reso è un buon modo per gestire i risultati multipli.

Di solito ti piacerebbe che un metodo restituisse solo una cosa, perché quella è la cosa più facile da implementare, capire e mantenere.

Questo ci lascia con entrambe le eccezioni per gestire gli errori o suddividere la chiamata di servizio in più chiamate di servizio.

Sono propenso a che Eccezioni sarebbe il modo "corretto" per il tuo scenario, a meno che tu ora i chiamanti chiamino frequentemente il servizio con parametri sbagliati e che danneggino così tanto le prestazioni, quindi sarebbe un problema. (la mia macchina può facilmente elaborare 10000 eccezioni al secondo).

In questo caso, suddividere la chiamata di servizio in più chiamate può essere una buona soluzione, pseudo-codice:

public ActionResult Resize(string path, int width, int height)
{
    if (resizeService.IsValidSize(width, height) == false)
    {
        return new HttpStatusCodeResult(403, "Invalid parameters.");
    }

    if (resizeService.IsValidPath(path) == false)
    {
        return new HttpStatusCodeResult(404, "File not found.");
    }

    var resizeResult = resizeService.Resize(path, width, height);
    return Content(resizeResult);
}

L'unico lato negativo nella mia mente è che se il file viene cancellato / spostato / bloccato, il file potrebbe trovarsi lì quando si chiama resizeService.IsValidPath, ma inaccessibile quando si verifica la chiamata di ridimensionamento effettiva che quindi genera un eccezione.

Vorrei approssimare la probabilità di tale errore e se è improbabile, preferirei mantenere il codice pulito senza aggiungere ulteriore gestione delle eccezioni.

    
risposta data 26.07.2017 - 07:58
fonte
1

Alcune brave persone qui hanno praticamente sottolineato che le eccezioni qui sarebbero un buon modo per andare e, personalmente, sono d'accordo con loro. Nondimeno, per l'argomento, farò finta di non essere completamente d'accordo con loro e suggerirò un approccio alternativo: usare il polimorfismo.

Invece di avere una clausola switch enorme che dovresti espandere ogni volta che aggiungi un nuovo tipo di errore, puoi semplicemente definire un'azione predefinita da eseguire quando viene creato il risultato. Qualcosa del genere:

public class ReadFileResult
{
    public virtual void Action()
    {
        throw new Exception("Unexpected file result");
    }
}

public class FileNotFoundResult : ReadFileResult
{
    public override void Action()
    {
        Console.WriteLine("File not found");
    }
}

public class BytesFileResult : ReadFileResult
{
    public BytesFileResult(byte[] fileData)
    {
        FileData = fileData;
    }

    public byte[] FileData { get; }

    public override void Action()
    {
        Console.WriteLine(bytesFileResult.FileData.Length); //do something with the data
    }
}

class Program
{
    static void Main(string[] args)
    {
        ReadFileResult readFileResult = ReadFile("test.txt");
        readFileResult.Action();           
    }

    public static ReadFileResult ReadFile(string filename)
    {
        throw new NotImplementedException(); //some logic here
    }
}

Ovviamente, alcune varianti possono essere fatte qui. ReadFileResult può essere una classe astratta o un'interfaccia, costringendo quindi un utente a implementare il tipo che deve essere restituito. Ho scelto qui l'implementazione predefinita che corrisponde al blocco predefinito nell'istruzione switch.

Inoltre, si noterà che Action() è un metodo senza parametri che non ha alcun tipo di ritorno e alcune azioni potrebbero garantire un tipo di ritorno o alcuni parametri. Questo può essere facilmente ottenuto utilizzando l'iniezione di dipendenza. Un buon esempio di questo è mostrato in BytesFileResult class. In alternativa, puoi fornire una raccolta di oggetti che puoi utilizzare come contenitore di qualunque cosa tu abbia bisogno di entrare o uscire dal metodo (non un fan di questa soluzione, ma può essere fatto).

    
risposta data 26.07.2017 - 08:43
fonte

Leggi altre domande sui tag