web-dev-qa-db-fra.com

Je ne comprends pas pourquoi nous avons besoin du "nouveau" mot clé

Je suis nouveau en C #, issu d'un arrière-plan C++. En C++, vous pouvez faire ceci:

class MyClass{
....
};
int main()
{
   MyClass object; // this will create object in memory
   MyClass* object = new MyClass(); // this does same thing
}

Alors qu'en C #:

class Program
{
    static void Main(string[] args)
    {
        Car x;
        x.i = 2;
        x.j = 3;
        Console.WriteLine(x.i);
        Console.ReadLine();

    }
}
class Car
{
    public int i;
    public int j;


}

tu ne peux pas faire ça. Je me demande pourquoi Car x ne fera pas son travail.

36
user6528398

Il y a beaucoup d'idées fausses ici, à la fois dans la question elle-même et dans les différentes réponses.

Permettez-moi de commencer par examiner la prémisse de la question. La question est "pourquoi avons-nous besoin du mot clé new en C #?" La motivation de la question est ce fragment de C++:

 MyClass object; // this will create object in memory
 MyClass* object = new MyClass(); // this does same thing

Je critique cette question pour deux raisons.

Tout d'abord, ceux-ci ne font pas la même chose en C++, donc la question est basée sur une mauvaise compréhension du langage C++. Il est très important de comprendre la différence entre ces deux choses en C++, donc si vous ne comprenez pas très clairement la différence, trouvez un mentor qui peut vous apprendre à savoir quelle est la différence, et quand les utiliser.

Deuxièmement, la question présuppose - à tort - que ces deux syntaxes font la même chose en C++, puis, curieusement, demande "pourquoi avons-nous besoin de new en C #?" Sûrement la bonne question à poser étant donné - encore une fois, la présupposition fausse - est "pourquoi avons-nous besoin de new en C++?" Si ces deux syntaxes font la même chose - ce qu'elles ne font pas - alors pourquoi avoir deux syntaxes en premier lieu?

La question est donc à la fois basée sur une fausse prémisse et la question sur C # ne découle pas réellement de la conception - mal comprise - de C++.

Ceci est un gâchis. Jetons cette question et posons de meilleures questions. Et posons la question sur C # qua C #, et non dans le contexte des décisions de conception de C++.

Que fait l'opérateur new X En C #, où X est un type classe ou struct? (Ignorons les délégués et les tableaux aux fins de cette discussion.)

Le nouvel opérateur:

  • Provoque l'allocation d'une nouvelle instance du type donné; les nouvelles instances ont tous leurs champs initialisés aux valeurs par défaut.
  • Provoque l'exécution d'un constructeur du type donné.
  • Produit une référence à l'objet alloué, si l'objet est un type de référence, ou la valeur elle-même si l'objet est un type de valeur.

Très bien, je peux déjà entendre les objections des programmeurs C #, alors rejetons-les.

Objection: aucun nouveau stockage n'est alloué si le type est un type valeur, je vous entends dire. Eh bien, la spécification C # n'est pas d'accord avec vous. Quand tu dis

S s = new S(123);

pour certains types de structure S, la spécification indique que un nouveau stockage temporaire est alloué sur le pool à court terme, initialisé à ses valeurs par défaut , le constructeur s'exécute avec this défini pour faire référence au stockage temporaire, puis l'objet résultant est copié dans s. Cependant, le compilateur est autorisé à utiliser une optimisation de copie-élision à condition qu'il puisse prouver qu'il est impossible que l'optimisation soit observée dans un programme sûr . (Exercice: déterminez dans quelles circonstances une élision de copie ne peut pas être effectuée; donnez un exemple d'un programme qui aurait des comportements différents si l'élision était ou n'était pas utilisée.)

Objection: une instance valide d'un type de valeur peut être produite en utilisant default(S); aucun constructeur n'est appelé, je vous entends dire. C'est correct. Je n'ai pas dit que new est la seule façon de créer une instance d'un type valeur.

En fait, pour une valeur de type new S() et default(S) sont la même chose.

Objection: un constructeur est-il vraiment exécuté pour des situations comme new S(), s'il n'est pas présent dans le code source en C # 6, je vous entends dire. C'est un "si un arbre tombe dans la forêt et que personne ne l'entend, est-ce qu'il fait du bruit?" question. Y a-t-il une différence entre un appel à un constructeur qui ne fait rien et aucun appel du tout? Ce n'est pas une question intéressante. Le compilateur est libre d'éliminer les appels qu'il sait ne rien faire.

Supposons que nous ayons une variable de type valeur. Doit-on initialiser la variable avec une instance produite par new?

Non. Les variables qui sont automatiquement initialisées , telles que les champs et les éléments du tableau, seront initialisées à la valeur par défaut - c'est-à-dire la valeur de la structure où tous les champs sont eux-mêmes leurs valeurs par défaut.

Les paramètres formels seront initialisés avec l'argument, évidemment.

Les variables locales de type valeur doivent être définitivement attribuées avec quelque chose avant la lecture des champs, mais il n'est pas nécessaire que ce soit une expression new .

Donc, efficacement, les variables de type valeur sont automatiquement initialisées avec l'équivalent de default(S), à moins qu'elles ne soient locales.

Oui.

Pourquoi ne pas faire de même pour les locaux?

L'utilisation d'un local non initialisé est fortement associée au code bogué. Le langage C # interdit cela car cela trouve des bogues.

Supposons que nous ayons une variable de type référence. Doit-on initialiser S avec une instance produite par new?

Non. Les variables d'initialisation automatique seront initialisées avec null. Les sections locales peuvent être initialisées avec n'importe quelle référence, y compris null, et doivent être définitivement attribuées avant d'être lues.

Donc, efficacement, les variables de type référence sont automatiquement initialisées avec null, sauf si elles sont locales?

Oui.

Pourquoi ne pas faire de même pour les locaux?

Même raison. Un bug probable.

Pourquoi ne pas initialiser automatiquement les variables de type référence en appelant automatiquement le constructeur par défaut? Autrement dit, pourquoi ne pas rendre R r; Identique à R r = new R();?

Eh bien, tout d'abord, de nombreux types n'ont pas de constructeur par défaut, ou d'ailleurs, n'importe quel constructeur accessible. Deuxièmement, il semble étrange d'avoir une règle pour un local ou un champ non initialisé, une autre règle pour un formel et encore une autre règle pour un élément de tableau. Troisièmement, la règle existante est très simple: une variable doit être initialisée à une valeur; cette valeur peut être tout ce que vous aimez; pourquoi l'hypothèse qu'une nouvelle instance est souhaitée est garantie? Ce serait bizarre si cela

R r;
if (x) r = M(); else r = N();

a provoqué l'exécution d'un constructeur pour initialiser r.

Laissant de côté la sémantique de l'opérateur new, pourquoi est-il nécessaire syntaxiquement pour avoir un tel opérateur?

Ce n'est pas. Il existe un certain nombre de syntaxes alternatives qui pourraient être grammaticales. Le plus évident serait d'éliminer tout simplement le new. Si nous avons une classe C avec un constructeur C(int) alors nous pourrions simplement dire C(123) au lieu de new C(123). Ou nous pourrions utiliser une syntaxe comme C.construct(123) ou quelque chose de ce genre. Il existe plusieurs façons de le faire sans l'opérateur new.

Alors pourquoi l'avoir?

Tout d'abord, C # a été conçu pour être immédiatement familier aux utilisateurs de C++, Java, JavaScript et d'autres langages qui utilisent new pour indiquer qu'un nouveau stockage est en cours d'initialisation pour un objet.

Deuxièmement, le bon niveau de redondance syntaxique est hautement souhaitable. La création d'objets est spéciale; nous voulons appeler quand cela arrive avec son propre opérateur.

47
Eric Lippert

En C #, vous pouvez faire la chose similaire:

  // please notice "struct"
  struct MyStruct {
    ....
  }

  MyStruct sample1; // this will create object on stack
  MyStruct sample2 = new MyStruct(); // this does the same thing

Rappelons que les primitives telles que int, double et bool sont également de type struct, donc même s'il est conventionnel d'écrire

  int i;

on peut aussi écrire

  int i = new int(); 

contrairement à C++, C # n'utilise pas de pointeurs (en mode sans échec) vers les instances, mais C # a les déclarations class et struct:

  • class: vous avez référence à l'instance, la mémoire est allouée sur tas, new est obligatoire; semblable à MyClass* in C++

  • struct: vous avez valeur, la mémoire est (généralement) allouée sur pile, new est facultatif; similaire à MyClass in C++

Dans votre cas particulier, vous pouvez simplement transformer Car en struct

struct Car
{
    public int i;
    public int j;
}

et donc le fragment

Car x; // since Car is struct, new is optional now 
x.i = 2;
x.j = 3;

sera correct

28
Dmitry Bychenko

En C #, les objets de type class sont toujours alloués sur le tas, c'est-à-dire que les variables de ces types sont toujours des références ("pointeurs"). Le simple fait de déclarer une variable d'un tel type ne provoque pas l'allocation d'un objet. Allouer un objet class sur la pile comme il est courant de le faire en C++ n'est pas (en général) une option en C #.

Les variables locales de tout type qui n'ont pas été affectées à sont considérées comme non initialisées et ne peuvent pas être lues tant qu'elles n'ont pas été affectées à. Il s'agit d'un choix de conception (une autre façon aurait été d'affecter default(T) à chaque variable au moment de la déclaration), ce qui semble être une bonne idée car cela devrait vous protéger contre certaines erreurs de programmation.

C'est similaire à la façon dont en C++, cela n'aurait pas de sens de dire SomeClass *object; et ne rien lui assigner.

Étant donné qu'en C # toutes les variables de type class sont des pointeurs, l'allocation d'un objet vide lorsque la variable est déclarée entraînerait un code inefficace lorsque vous ne souhaitez en fait attribuer une valeur à la variable que plus tard, par exemple dans des situations comme celle-ci:

// Needs to be declared here to be available outside of `try`
Foo f;

try { f = GetFoo(); }
catch (SomeException) { return null; }

f.Bar();

Ou

Foo f;

if (bar)
    f = GetFoo();
else
    f = GetDifferentFoo();
15
Matti Virkkunen

ignorer le côté pile vs tas de choses:

parce que C # a pris la mauvaise décision de copier C++ alors qu'ils auraient dû juste faire la syntaxe

Car car = Car()

(ou quelque chose de similaire). Avoir du "nouveau" est superflu.

12
zacaj

Lorsque vous utilisez des types référencés, dans cette instruction

Car c = new Car();

deux entités sont créées: une référence nommée c à un objet de type Car dans la pile et à l'objet de type Car lui-même dans le tas.

Si vous voulez juste écrire

Car c;

vous créez ensuite une référence non initialisée (à condition que c soit une variable locale) qui pointe vers nulle part.

En fait, il est équivalent au code C++ où au lieu de références, il y a des pointeurs utilisés.

Par exemple

Car *c = new Car();

ou juste

Car *c;

La différence entre C++ et C # est que C++ peut créer des instances de classes dans la pile comme

Car c;

En C #, cela signifie créer une référence de type Car qui, comme je l'ai dit, ne pointe nulle part.

7
Vlad from Moscow

Dans le guide de programmation Microsoft:

Au moment de l'exécution, lorsque vous déclarez une variable d'un type de référence, la variable contient la valeur null jusqu'à ce que vous créez explicitement une instance de l'objet à l'aide du nouvel opérateur ou que vous lui affectiez un objet qui a été créé ailleurs à l'aide de new

Une classe est un type de référence. Lorsqu'un objet de la classe est créé, la variable à laquelle l'objet est affecté ne contient qu'une référence à cette mémoire. Lorsque la référence d'objet est affectée à une nouvelle variable, la nouvelle variable fait référence à l'objet d'origine. Les modifications apportées via une variable sont reflétées dans l'autre variable car elles font toutes deux référence aux mêmes données.

Un struct est un type de valeur. Lorsqu'une structure est créée, la variable à laquelle la structure est affectée contient les données réelles de la structure. Lorsque la structure est affectée à une nouvelle variable, elle est copiée. La nouvelle variable et la variable d'origine contiennent donc deux copies distinctes des mêmes données. Les modifications apportées à une copie n'affectent pas l'autre copie.

Je pense que dans votre exemple C #, vous essayez efficacement d'attribuer des valeurs à un pointeur nul. En traduction c ++, cela ressemblerait à ceci:

Car* x = null;
x->i = 2;
x->j = 3;

Cela compilerait évidemment, mais planterait.

2
Peter Respondek