Tout d'abord, dans cette question, je voudrais rester à l'écart de la polémique sur la question de savoir si le commentaire du code source est bon ou mauvais. J'essaie simplement de comprendre plus clairement ce que les gens veulent dire lorsqu'ils parlent de commentaires qui vous disent POURQUOI, QUOI ou COMMENT.
Nous voyons souvent des directives comme "Les commentaires devraient vous dire POURQUOI; le code lui-même devrait vous dire COMMENT". Il est facile d'être d'accord avec l'énoncé à un niveau abstrait. Cependant, les gens laissent généralement tomber comme un dogme et quittent la pièce sans autre explication. J'ai vu cela utilisé dans tellement d'endroits et de contextes différents, qu'il semble que les gens peuvent s'entendre sur le slogan, mais ils semblent parler de choses complètement différentes.
Donc, revenons à la question: si les commentaires vous disent POURQUOI, de quoi s'agit-il POURQUOI nous parlons? Est-ce la raison pour laquelle ce morceau de code existe en premier lieu? Est-ce ce que devrait faire ce code pièce? J'apprécierais vraiment que quelqu'un puisse donner une explication claire, puis ajouter de bons exemples (de mauvais exemples ne sont pas vraiment nécessaires, mais n'hésitez pas à les ajouter pour le contraste).
De nombreuses questions se posent quant à savoir si les commentaires sont bons ou mauvais, mais personne ne répond à la question spécifique de savoir quels sont les bons exemples de commentaires qui vous disent POURQUOI.
L'exemple le plus courant et le plus distinctif est les commentaires sur diverses solutions de contournement. Par exemple celui-ci:
https://github.com/git/git/blob/master/compat/fopen.c :
/* * The order of the following two lines is important. * * FREAD_READS_DIRECTORIES is undefined before including git-compat-util.h * to avoid the redefinition of fopen within git-compat-util.h. This is * necessary since fopen is a macro on some platforms which may be set * based on compiler options. For example, on AIX fopen is set to fopen64 * when _LARGE_FILES is defined. The previous technique of merely undefining * fopen after including git-compat-util.h is inadequate in this case. */ #undef FREAD_READS_DIRECTORIES #include "../git-compat-util.h"
Vous trouverez sûrement plus d'exemples dans les sources Git et Linux; les deux projets essaient de suivre cette règle.
Je recommande également de suivre cette règle encore plus strictement avec les journaux de validation . Pour les commentaires de code, il peut arriver que vous corrigiez le code, mais oubliez de mettre à jour le commentaire. Avec la quantité de code dans le projet habituel, il est garanti que cela se produise tôt ou tard. D'autre part, le journal de validation est lié à la modification particulière et peut être rappelé à l'aide de la fonctionnalité "annoter"/"blâmer" du système de contrôle de version. Encore une fois, Git et Linux ont de bons exemples.
Regardez par exemple à ce commit . (ne pas copier ici, c'est trop long). Il a quatre paragraphes qui prennent presque toute la page (et un peu trop d'écran) décrivant ce qui était exactement faux et pourquoi c'était faux et qui continue et modifie toutes les grosses lignes SIX. Ils utilisent des commentaires comme celui-ci à deux fins:
(Remarque: il m'a fallu au plus 10 minutes de navigation aléatoire sur le dépôt git pour arriver à ces deux exemples, il serait donc facile d'en trouver plus ici)
Un commentaire qui vous indique pourquoi explique le raisonnement derrière le code - par exemple:
// We need to sync the values if the temp <doodad> GUID matches one of the active <doodad>'s
// GUID, as the temp <doodad> has the most recent values according to the server and said
// values might have changed since we added the <doodad>. We want a user to be able to <foo>
// the <doodad> whenever, which means those values must be accurate.
for (doodad in doodads) {
if ([doodad guid] == [tempDoodad guid]) {
[doodad updateFromDoodad:tempDoodad];
break;
}
}
Un commentaire qui vous indique comment explique ce que fait le code.
// Loop through our <doodads> and check for a GUID match. If it matches, copy the new values
// on the <doodad> that matches
for (doodad in doodads) {
if ([doodad guid] == [tempDoodad guid]) {
[doodad updateFromDoodad:tempDoodad];
break;
}
}
La différence est qu'un mainteneur peut regarder le premier et dire: "Oh, donc cela pourrait être obsolète!" Dans le second cas, ledit responsable a un commentaire qui ne vous dit rien que le code lui-même ne révèle pas (en supposant de bons noms de variables).
Voici un exemple réel d'un commentaire pourquoi, à partir d'un code iOS, j'ai travaillé sur l'endroit où nous devions obtenir une adresse de passerelle (ou une estimation raisonnable pour cela). J'aurais pu laisser les commentaires qui disaient des choses comme "Initialiser le socket de réception", mais cela ne ferait que dire à un responsable (ou à moi) ce qui se passait, pas pourquoi j'ai dû faire cet étrange kludge pour obtenir l'adresse de la passerelle dans le première place.
/*
We're going to do something really hacky here and use a custom partial
implementation of traceroute to get our gateway IP address.
[rant removed - irrelevant to the point]
There's no good way to get at the gateway address of an iDevice
right now. So, we have two options (per https://devforums.Apple.com/message/644915#644915 ):
1. Get at and parse the routing table (like netstat -rn, or route -n)
2. Do a traceroute and grab the IP address for the first hop
As far as I can tell, the former requires <sys/route.h> from the Mac OS X
header files, which doesn't seem like a good idea to me. Also, there's a
thread on the Apple Developer forums that seems to imply that header isn't
in iOS for a reason (https://devforums.Apple.com/message/774731#774731 ).
So when we send our request with a TTL of one it will survive a single hop
to the router and return, triumphant, with the router's IP address!
Viva la kludge!
PS: Original source was the below SO question, but I've modded it since then.
http://stackoverflow.com/questions/14304581/Hops-tracing-ttl-reciveform-on-ios/14304923#14304923
*/
// Default to using Google's DNS address. We used to try checking www.google.com
// if reachability reported we had internet, but that could still hang on routers
// that had no internet connectivity - not sure why.
const char *ip_addr = [kGoogleDNS UTF8String]; // Must be const to avoid undefined behavior
struct sockaddr_in destination,fromAddr;
int recv_sock;
int send_sock;
// ... more code follows
Je voudrais commencer ma réponse par une citation faite par Jeff Atwood dans son article de blog Le code vous dit comment, les commentaires vous expliquent pourquoi :
les meilleurs types de commentaires sont ceux dont vous n'avez pas besoin
Il déclare également que:
Vous devez d'abord vous efforcer de rendre votre code aussi simple que possible à comprendre sans compter sur les commentaires comme une béquille. Ce n'est qu'au point où le code ne peut pas être rendu plus facile à comprendre que vous commencez à ajouter des commentaires.
Je suis totalement d'accord et à ce stade, je dois ajouter qu'avant de commencer à rendre le code aussi simple que possible, je fais fonctionner le code, puis je commence à refactoring. Donc, lors de la première exécution avant de refactoriser l'ajout pourquoi les commentaires aident beaucoup.
Par exemple, si vous utilisez 3 boucles imbriquées avec des tables de hachage bidimensionnelles pour remplir un tableau des jours de la semaine lors de l'analyse des données, il est très facile de perdre la trace de ce qui a été fait par quelqu'un ou même par vous-même s'il n'est pas consulté pendant quelques semaines et soudainement refactorisé.
[loop1]6oclock -> [loop2]Monday -> [loop3]stage 1 to 4
-> tuesday-> stage 1 to 4
...
-> Saturday -> stage 1 to 4
7oclock -> Monday-> stage 1 to 4
....etc.
La partie supérieure étant un exemple de la façon dont 3 boucles imbriquées fonctionneraient avant la refactorisation.
Expliquer également certaines conditions de branche peut aider à mieux comprendre le code avec ce que l'on pensait dans le processus:
// added a zero before the actual day in order for the days always to be 2 digits long.
if( actualDayFuture < 10 )
{
actualDayFuture = padIfSingleDigitDate(actualDayFuture);
}
Même un code simple et évident fonctionne bien avec les commentaires. Juste pour rendre les choses un peu plus évidentes, plus claires ou plus faciles à comprendre pour les collègues et même pour vous-même dans la maintenance du logiciel.
Bien sûr, xp déclare avoir un code qui s'explique de lui-même, mais est-ce qu'un commentaire sur une ligne fait mal?
Je trouve également les règles suivantes de ce blog très utiles:
- Comprendre le matériel avant d'écrire
- Écrivez comme si votre public était un élève de quatrième année
- Pensez à la façon dont les lecteurs pourraient vous mal interpréter
Quiconque doit revenir dans son propre code ou quelqu'un d'autre ou même du code hérité sait que cela peut être un casse-tête. Donc, au lieu d'être paresseux ou d'essayer d'être un uber-codeur en ne commentant rien ou très peu, pourquoi ne pas vous faciliter la vie ou celle d'un pauvre bougre, qui doit maintenir votre code, la vie future en suivant les règles citées.
De nombreuses décisions de programmation prises sont également mises en doute lors des révisions et il n'est pas toujours clair pourquoi certaines parties ont été écrites telles quelles, même si certaines sections de code sont vitales pour qu'un programme fonctionne en raison d'un bogue majeur détecté car le code a été utilisé au fil des ans. . Donc, pour ne pas vous ennuyer complètement avec un tl; dr fermez avec une dernière citation de acmqueue :
Une documentation préalable, claire et complète est un élément clé dans la création de logiciels capables de survivre et de s'adapter. La documentation selon des normes élevées réduira le temps de développement, améliorera le travail et améliorera les résultats. Il est difficile de demander plus que cela à n'importe quelle technique.
J'ai tendance à réduire les commentaires à des références où une certaine fonctionnalité/code est expliqué plus en détail ou à expliquer pourquoi une certaine manière de programmation est choisie.
Étant donné que d'autres programmeurs ayant des compétences similaires utilisent ou lisent votre code, il est important de commenter si vous utilisez une méthode différente de celle attendue pour réaliser quelque chose. Vous pouvez donc expliquer dans un commentaire pourquoi vous choisissez de cette façon.
Par exemple, si vous pouvez utiliser deux capteurs différents sur un appareil Android et que l'un d'entre eux ne correspond pas à vos besoins, vous pouvez expliquer dans le commentaire pourquoi vous avez choisi l'autre.
Ainsi, le "pourquoi" devrait donner un justification sur vos choix.
Les commentaires devraient vous dire ce que le code ne fait pas, pas nécessairement délimité par POURQUOI , COMMENT =, ou QUOI . Si vous avez de bons noms et des fonctions bien délimitées, il est fort possible que le code vous dise exactement ce qui se passe. Par exemple:
List<LightMap> maps = makeLightmaps(receivingModels);
TrianglePartitioner partition = new Octree(castingTriangles);
List<Photon> photons = firePhotons(lights, partition);
if (photons.Count > 0)
{
PhotonPartitioner photonMap = new KDTree(photons);
gatherPhotons(maps, photonMap, partition, lights);
}
Ce code n'a vraiment pas besoin de commentaires. Les noms de fonction et de type facilitent la compréhension.
Parfois, cependant, il peut être difficile, voire impossible, de créer un code couramment comme celui ci-dessus. Par exemple, l'extrait de code suivant sert à trouver un point statistiquement aléatoire sur une sphère. Le calcul est assez opaque, donc un commentaire avec un lien vers l'explication est d'aider à dire COMMENT cela fonctionne. Cela peut être enveloppé dans une fonction pour dire CE QUE il fait sans avoir besoin d'un commentaire si nécessaire plus d'une fois, sinon le titre du lien aide également dans ce département.
double randomA = localGenerator.NextDouble();
double randomB = localGenerator.NextDouble();
//http://mathworld.wolfram.com/SpherePointPicking.html
double theta = 2 * Math.PI * randomA;
double phi = Math.Acos(2 * randomB - 1);
Vector3 randomDirection = new Vector3(Settings.ambientRayLength * (float)(Math.Cos(theta) * Math.Sin(phi)),
Settings.ambientRayLength * (float)(Math.Sin(theta) * Math.Sin(phi)),
Settings.ambientRayLength * (float)Math.Cos(phi));
Un autre exemple de cas où les commentaires vous disent ce que le code ne fait pas est d'expliquer une décision. Dans l'exemple suivant, le code ne verrouille pas une variable non locale au thread dans un morceau de code threadé. Il y a une raison à cela et le commentaire explique POURQUOI . Sans le commentaire, il pourrait être considéré comme un bug, ou même ne pas être remarqué.
Random random = new Random();
Parallel.For(0, maxPhotons, delegate(int photonIndex, ParallelLoopState state)
{
...
//I don't actually care if this random number is unique between threads, threadsafty is not that big of a deal
// in this case and locking the random object could cause a lot of lock contention
while (random.NextDouble() > reflectProbability)
{
...
}
...
}
Il pourrait peut-être être amélioré pour dire pourquoi l'objet aléatoire n'est pas créé à l'intérieur de la boucle parallèle en premier lieu. S'il n'y a pas de raison, cela pourrait également inciter quelqu'un à se rendre compte que l'idée est stupide et est un bon endroit pour refactoring.
Il peut être utile de reconnaître différents types de "pourquoi" - notamment:
Raisons pour lesquelles un code qui semble trop complexe ne fonctionnerait pas s'il était simplifié (par exemple, un transtypage apparemment superflu peut être nécessaire pour garantir que le code fonctionne dans certains cas d'angle).
Raisons pour lesquelles une opération simple particulière qui semble dangereuse est en fait sûre (par exemple, "notre routine de récupération de données rapportera un élément factice passé le dernier comme étant moins qu'autre chose, et l'élément après cela comme étant plus grand; tout élément qui devrait trier avant un autre, dans une séquence ascendante ou descendante cohérente, suivra au moins un autre élément (éventuellement factice) ").
Dans de nombreux cas, un commentaire du deuxième type dans une partie du code peut "correspondre" avec un commentaire du premier type dans une autre (par exemple "Bien qu'il semble que cette séquence d'opérations puisse être simplifiée, la routine Fitz s'appuie sur le Wongle n'est pas Woozled jusqu'à ce que le Bandersnatch ait été Blarped. ")
Tous mes commentaires ne sont pas du type "pourquoi", mais beaucoup le sont.
Voici des exemples d'un fichier source (Delphi):
// For easier access to the custom properties:
function GetPrivate: Integer; // It's an integer field in the external program so let's treat it like that here
// The below properties depend on the ones above or are calculated fields.
// They are kept up-to-date in the OnEventModified event of the TTSynchronizerStorage
// or in the ClientDataSet.OnCalcFields of the TcxDBSchedulerStorage.DataSource.DataSet
property IsModified : Boolean read GetIsModified write SetIsModified;
property IsCatTT : Boolean read GetIsCatTT write SetIsCatTT;
property IsSynced : Boolean read GetIsSynced write SetIsSynced;
lLeftPos := pos(' - [',ASubject); // Were subject and [shiftnaam:act,project,cust] concatenated with a dash?
// Things that were added behing the ] we will append to the subject:
// In the storage the custom value must also be set for:
Self.SetCustomFieldValueByname(cCustFldIsCatTT,Result);
// When we show the custom fields in a grid, the Getters are not executed,
// because the DevEx code does not know about our class helpers.
// So we have two keep both properties synchronized ourselves:
// lNewMasterEvent was set to usUpdated, overwrite because we added:
if ARepair then
lNewMasterEvent.CustUpdateStatus := usRecreated
// The source occurrence date may have bee changed. Using GetOriginalDate we can retrieve the original date,
// then use that for creating a target occurrence (and update its date):
lNewTTOccurrence.CustSyncEntryID := cSyncEntryID0; // Backward compatibility with old sync methode
// Single event became recurring or vice versa; replace entire event
// In contradiction to CopySingleEventToTimeTell, CopyMasterEventToTimeTell does not have a ANewStatus parameter
// because master events are always added.
Notez que (mes) pourquoi commentaires précèdent généralement le code qui va le faire (donc se terminent par deux points).
J'ai des commentaires expliquant seulement quoi se passe, par exemple lorsqu'un processus comporte de nombreuses étapes qui ont un regroupement logique (et que le code n'est pas refactorisé pour le montrer automatiquement), je commenterai comme:
// Step 1. Initialization
N'oubliez pas, si vous écrivez un programme, vous n'êtes pas seulement en train de taper au hasard, vous le faites parce que vous avez un modèle de ce que vous voulez, que ce soit dans un document formel ou juste dans votre tête. Les trucs dans votre tête sont tout aussi réels que les logiciels/données d'un ordinateur (et tout aussi susceptibles de contenir des bogues).
Une personne qui lit votre code peut ne pas avoir ce modèle en tête, donc les commentaires peuvent servir à lui dire quel était le modèle et comment le code s'y rapporte. Je pense que c'est ce que l'on entend par "pourquoi". Certes, il est bon de rendre le code lui-même aussi explicite que possible, mais ce n'est pas toujours suffisant. Exemple:
// transform the x,y point location to the nearest hexagonal cell location
ix1 = (int)floor(0.5 + x + y/2);
iy1 = (int)floor(0.5 + y);
En plus de cela, le modèle change avec le temps et ces changements doivent être transférés dans le code. Ainsi, les commentaires doivent non seulement dire "pourquoi" quelque chose est dans le code, mais tout aussi important comment le changer en réponse aux changements de modèle prévus. Exemple:
// to change to square cell locations, remove the "+ y/2" in the above code
Je pense que le but des commentaires est parfois négligé.
Je comprends le POURQUOI comme étant la raison pour laquelle vous faites quelque chose d'une manière peut-être étrange ou peut-être illogique, en raison des circonstances données qui l'exigent. Le COMMENT peut être vu dans le code lui-même, peu importe à quel point il est étrange, même si le code n'a aucun "sens". Le CE QUE est probablement mieux dit au début de la documentation de la classe/fonction. Cela vous laisse donc ajouter le POURQUOI , où vous expliquez tout ce qui n'est pas inclus dans le COMMENT et le QUOI, et les façons particulières que vous devez prendre pour des raisons au-delà votre contrôle.
Bien sûr, ce n'est pas toujours le cas, en dehors du pays des licornes et des arcs-en-ciel ...
COMMENT:
foreach($critters as $creature) {
$creature->dance();
}
QUOI:
/* Dancing creatures v1.0
*
* The purpose of this is to make all your critters do the funky dance.
*/
foreach($critters as $creature) {
$creature->dance();
}
POURQUOI:
// We had to store the items in an array of objects because of _____ (reason)
foreach($critters as $creature) {
$creature->dance();
}
J'ai toujours appris à écrire des commentaires dans des fichiers d'en-tête C++ (car il n'est pas toujours clair CE QUE fait une fonction, même si le nom donne un bon indice), surtout si vous transmettez une API à d'autres développeurs ou utilisez un outil d'autodoc comme doxygen.
Donc, pour moi, un commentaire typique ressemble à quelque chose
/*** Functionname
/* What happens here
/* [in] Params
/* [out] params
/***
La seule fois où j'ai utilisé POURQUOI des commentaires, c'est des choses difficiles à saisir et parfois même pour le programmeur, comme "NE PAS TOUCHER CECI! Parce que ..." ou "LE PROGRAMME CRASHERA SI LA LIGNE IS SUPPRIMÉ ... "
Les solutions de contournement, les hacks et les comportements étranges remplissent les critères POURQUOI à mes yeux ...
Un très bon exemple, même hilarant, est cette "solution de contournement" pour du code falsifié écrit par une personne nommée Richard, quelqu'un d'autre l'a enveloppé et a expliqué pourquoi dans les commentaires ... https://stackoverflow.com/a/184673/979785
Malheureusement, il y a plusieurs fois, où vous êtes obligé d'envelopper le taureau **** parce que vous ne pouvez pas toucher l'original, soit parce que "ça a toujours été comme ça" soit vous n'y avez pas accès ou ... eh bien, vous n'ont pas le temps de réparer l'original dans le but de ne pas vraiment qualifier pour les frais généraux.
Le code est censé spécifier le plan d'exécution. De cette façon, le suiveur de programme (ou le compilateur) peut comprendre quoi faire et comment le faire. Ce qui se décompose en étapes que le suiveur de programme peut suivre. Les étapes primitives sont le comment.
L'intention du codeur est une autre affaire. Dans un code simple, clair et direct, l'intention est évidente. Tout lecteur humain raisonnablement compétent arrivera à l'intention d'un bloc de code, simplement en lisant le code. La plupart du code devrait se lire comme ceci.
Parfois, la relation entre l'intention et le plan est obscure. Le code révèle le quoi et le comment, mais pas le pourquoi. C'est alors que les commentaires qui révèlent l'intention valent la peine. L'intention du programmeur est le pourquoi.
Ce problème se déroule actuellement dans les procédures stockées et les vues par rapport à un modèle de données complexe et quelque peu compliqué.
Nous avons (nombreux) composé des sélections comme "Cas où x.account n'est pas nul et x.address dans (sélectionnez l'adresse de fedex) puis x.account sinon y.account end" tout au long et la productivité est attendue bien qu'il n'y ait pas de temps à tous à lire tout le code source. Et cet exemple est en quelque sorte logique, mais il est toujours impénétrable.
Les commentaires expliquant pourquoi si dans fedex alors x et sinon alors y - éclairent l'ensemble du système et quand nous en lisons suffisamment, nous commençons à l'obtenir. Et cela est trop simplifié et il y a des centaines ou des milliers de déclarations similaires. Mon cœur brille chaleureusement vers celui qui a été le bon développeur de 2007 qui a mis ces pourquoi.
Alors oui, des modèles de données complexes alambiqués et des vues velues et des procédures stockées avec plusieurs chemins valablement nommés, s'il vous plaît, pour l'amour de D.ieu, dites-nous pourquoi.
Je viens d'écrire ce commentaire; c'est un exemple concret d'explication pourquoi une ligne de code est ce qu'elle est, et en particulier pourquoi je l'ai changé.
La méthode examine les données stockées et évalue si elles sont complètes jusqu'à ce jour à une extrémité et jusqu'à la date de début à l'autre extrémité.
// In principal, this should be ">=", as we may have data up to the account start
// date but not complete for that day; in practice, 98% of the time if we have
// data for the start date it *is* complete, and requerying it would be a waste
// of time.
while (endDate > accountStartDate)
...
Comme vous pouvez probablement le deviner, l'opérateur supérieur à avait été supérieur ou égal. Le commentaire explique pourquoi l'ancienne valeur est logique et pourquoi la nouvelle valeur est meilleure. Si quelqu'un regarde cela à l'avenir, il verra que l'utilisation de ">" n'est pas un oubli, mais une optimisation. Ils peuvent ensuite le modifier ou le laisser, en fonction des besoins à ce moment-là.