Renvoi de deux valeurs, Tuple vs 'out' vs 'struct'

86

Prenons une fonction qui renvoie deux valeurs. Nous pouvons écrire:

// Using out:
string MyFunction(string input, out int count)

// Using Tuple class:
Tuple<string, int> MyFunction(string input)

// Using struct:
MyStruct MyFunction(string input)

Laquelle est la meilleure pratique et pourquoi?

Xaqron
la source
La chaîne n'est pas un type valeur. Je pense que vous vouliez dire "considérez une fonction qui renvoie deux valeurs".
Eric Lippert
@Eric: Vous avez raison. Je voulais dire des types immuables.
Xaqron
et quel est le problème avec une classe?
Lukasz Madon
1
@lukas: Rien, mais ce n'est sûrement pas dans les meilleures pratiques. C'est une valeur légère (<16 Ko) et si je veux ajouter un code personnalisé, je vais utiliser structcomme Ericmentionné.
Xaqron
1
Je dirais que n'utiliser que lorsque vous avez besoin de la valeur de retour pour décider si vous devez traiter les données de retour, comme dans TryParse, sinon vous devez toujours renvoyer un objet structuré, comme si l'objet structuré devait être de type valeur ou une référence le type dépend de l'utilisation supplémentaire que vous faites des données
MikeT

Réponses:

93

Ils ont chacun leurs avantages et leurs inconvénients.

Les paramètres de sortie sont rapides et bon marché, mais nécessitent que vous passiez une variable et reposiez sur une mutation. Il est presque impossible d'utiliser correctement un paramètre out avec LINQ.

Les tuples exercent une pression sur le ramassage des ordures et ne sont pas auto-documentés. «Item1» n'est pas très descriptif.

Les structures personnalisées peuvent être lentes à copier si elles sont volumineuses, mais sont auto-documentées et efficaces si elles sont petites. Cependant, il est également difficile de définir tout un tas de structures personnalisées pour des utilisations triviales.

Je serais enclin à la solution de structure personnalisée toutes choses étant égales par ailleurs. Encore mieux, c'est de créer une fonction qui ne renvoie qu'une seule valeur . Pourquoi renvoyez-vous deux valeurs en premier lieu?

MISE À JOUR: Notez que les tuples en C # 7, qui ont été livrés six ans après la rédaction de cet article, sont des types de valeur et donc moins susceptibles de créer une pression de collecte.

Eric Lippert
la source
2
Le renvoi de deux valeurs est souvent un substitut pour ne pas avoir de types d'options ou ADT.
Anton Tykhyy
2
D'après mon expérience avec d'autres langues, je dirais que les tuples sont généralement utilisés pour le regroupement rapide et sale d'éléments. Il est généralement préférable de créer une classe ou une structure simplement parce qu'elle vous permet de nommer chaque élément. Lorsque vous utilisez des tuples, la signification de chaque valeur peut être difficile à déterminer. Mais cela vous évite de prendre le temps de créer la classe / structure qui peut être exagérée si ladite classe / structure ne serait pas utilisée ailleurs.
Kevin Cathcart
23
@Xaqron: Si vous trouvez que l'idée de "données avec un délai d'expiration" est courante dans votre programme, vous pouvez envisager de créer un type générique "TimeLimited <T>" afin que votre méthode retourne une <string> ou TimeLimited <Uri> ou peu importe. La classe TimeLimited <T> peut alors avoir des méthodes d'assistance qui vous indiquent "combien de temps nous reste-t-il?" ou "est-il expiré?" ou peu importe. Essayez de capturer une sémantique intéressante comme celle-ci dans le système de types.
Eric Lippert
3
Absolument, je n'utiliserais jamais Tuple dans le cadre d'une interface publique. Mais même pour le code `` privé '', j'obtiens une lisibilité énorme à partir d'un type approprié au lieu d'utiliser Tuple (en particulier à quel point il est facile de créer un type interne privé avec Auto Properties).
SolutionYogi
2
Que signifie la pression de collecte?
roule
25

En plus des réponses précédentes, C # 7 apporte des tuples de type valeur, contrairement à ce System.Tuplequi est un type de référence et offre également une sémantique améliorée.

Vous pouvez toujours les laisser sans nom et utiliser la .Item*syntaxe:

(string, string, int) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.Item1; //John
person.Item2; //Doe
person.Item3;   //42

Mais ce qui est vraiment puissant dans cette nouvelle fonctionnalité, c'est la possibilité d'avoir des tuples nommés. Nous pourrions donc réécrire ce qui précède comme ceci:

(string FirstName, string LastName, int Age) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.FirstName; //John
person.LastName; //Doe
person.Age;   //42

La destruction est également prise en charge:

(string firstName, string lastName, int age) = getPerson()

dimlucas
la source
2
Ai-je raison de penser que cela renvoie essentiellement une structure avec des références en tant que membres sous le capot?
Austin_Anderson
4
savons-nous comment la performance de cela se compare à l'utilisation de paramètres?
SpaceMonkey
20

Je pense que la réponse dépend de la sémantique de ce que fait la fonction et de la relation entre les deux valeurs.

Par exemple, les TryParseméthodes prennent un outparamètre pour accepter la valeur analysée et renvoient un boolpour indiquer si l'analyse a réussi ou non. Les deux valeurs n'appartiennent pas vraiment ensemble, donc, sémantiquement, cela a plus de sens, et l'intention du code est plus facile à lire, d'utiliser le outparamètre.

Si, cependant, votre fonction renvoie les coordonnées X / Y d'un objet à l'écran, alors les deux valeurs appartiennent sémantiquement ensemble et il serait préférable d'utiliser a struct.

J'éviterais personnellement d'utiliser un tuplepour tout ce qui sera visible pour le code externe en raison de la syntaxe maladroite pour récupérer les membres.

Andrew Cooper
la source
+1 pour la sémantique. Votre réponse est plus appropriée avec les types de référence lorsque nous pouvons laisser le outparamètre null. Il existe quelques types immuables nullables.
Xaqron
3
En fait, les deux valeurs de TryParse vont très bien ensemble, bien plus qu'impliqué en ayant l'une comme valeur de retour et l'autre comme paramètre ByRef. À bien des égards, la chose logique à renvoyer serait un type Nullable. Il y a des cas où le modèle TryParse fonctionne bien, et d'autres où c'est pénible (c'est bien qu'il puisse être utilisé dans une instruction "if", mais il y a de nombreux cas où le retour d'une valeur Nullable ou la possibilité de spécifier une valeur par défaut serait plus pratique).
supercat le
@supercat je serais d'accord avec andrew, ils ne vont pas ensemble. bien qu'ils soient liés, le retour vous indique si vous devez vous soucier de la valeur, pas quelque chose qui doit être traité en tandem. Ainsi, après avoir traité le retour, il n'est plus nécessaire pour tout autre traitement lié à la valeur de sortie, c'est différent de renvoyer un KeyValuePair à partir d'un dictionnaire où il existe un lien clair et continu entre la clé et la valeur. bien que je sois d'accord si les types nullables ont été dans .Net 1.1, ils les auraient probablement utilisés comme null serait un moyen approprié de ne
signaler
@MikeT: Je considère qu'il est exceptionnellement malheureux que Microsoft ait suggéré que les structures ne soient utilisées que pour des choses représentant une seule valeur, alors qu'en fait une structure à champ exposé est le support idéal pour transmettre ensemble un groupe de variables indépendantes liées avec du ruban adhésif. . L'indicateur de succès et la valeur sont significatifs ensemble au moment où ils sont renvoyés , même si après cela, ils seraient plus utiles en tant que variables séparées. Les champs d'une structure de champs exposés stockés dans une variable sont eux-mêmes utilisables en tant que variables séparées. Dans tous les cas ...
supercat
@MikeT: En raison de la manière dont la covariance est et n'est pas prise en charge dans le cadre, le seul trymodèle qui fonctionne avec les interfaces covariantes est T TryGetValue(whatever, out bool success); cette approche aurait permis des interfaces IReadableMap<in TKey, out TValue> : IReadableMap<out TValue>, et laisserait le code qui veut mapper des instances de Animalà des instances de Card'accepter un Dictionary<Cat, ToyotaCar>[using TryGetValue<TKey>(TKey key, out bool success). Une telle variance n'est pas possible si elle TValueest utilisée comme refparamètre.
supercat
2

J'irai avec l'approche d'utilisation du paramètre Out, car dans la deuxième approche, vous auriez besoin de créer un objet de la classe Tuple, puis d'y ajouter de la valeur, ce qui, à mon avis, est une opération coûteuse par rapport au retour de la valeur du paramètre out. Bien que si vous souhaitez renvoyer plusieurs valeurs dans Tuple Class (ce qui ne peut pas être accompli en renvoyant simplement un paramètre de sortie), je vais opter pour la deuxième approche.

Sumit
la source
Je suis d'accord avec out. De plus, il y a un paramsmot clé que je n'ai pas mentionné pour garder la question claire.
Xaqron
2

Vous n'avez pas mentionné une autre option, qui consiste à avoir une classe personnalisée au lieu de struct. Si les données sont associées à une sémantique qui peut être exploitée par des fonctions, ou si la taille de l'instance est suffisamment grande (> 16 octets en règle générale), une classe personnalisée peut être préférée. L'utilisation de "out" n'est pas recommandée dans l'API publique en raison de son association à des pointeurs et nécessitant une compréhension du fonctionnement des types de référence.

https://msdn.microsoft.com/en-us/library/ms182131.aspx

Tuple est bon pour un usage interne, mais son utilisation est délicate dans l'API publique. Donc, mon vote est entre struct et class pour l'API publique.

JeanP
la source
1
Si un type existe uniquement dans le but de renvoyer une agrégation de valeurs, je dirais qu'un simple type de valeur de champ exposé est le plus clair pour ces sémantiques. S'il n'y a rien dans le type autre que ses champs, il n'y aura aucune question sur les types de validation de données qu'il effectue (aucune, évidemment), s'il représente une vue capturée ou en direct (une structure en champ ouvert ne peut pas agir comme une vue en direct), etc. Les classes immuables sont moins pratiques à utiliser et n'offrent un avantage en termes de performances que si les instances peuvent être transmises plusieurs fois.
supercat du
0

Il n'y a pas de «meilleure pratique». C'est ce avec quoi vous êtes à l'aise et ce qui fonctionne le mieux dans votre situation. Tant que vous êtes cohérent avec cela, aucune des solutions que vous avez publiées ne pose de problème.

rusé
la source
3
Bien sûr, ils fonctionnent tous. Au cas où il n'y aurait pas d'avantage technique, je suis toujours curieux de savoir ce qui est le plus utilisé par les experts.
Xaqron