Jak właściwie działa konstruktor const?

112

Zauważyłem, że możliwe jest utworzenie konstruktora const w Dart. W dokumentacji jest napisane, że constsłowo jest używane do oznaczenia czegoś będącego stałą czasową kompilacji.

Zastanawiałem się, co się dzieje, gdy używam constkonstruktora do tworzenia obiektu. Czy to jest jak niezmienny obiekt, który jest zawsze taki sam i dostępny w czasie kompilacji? Jak constwłaściwie działa koncepcja konstruktora? Czym różni się konstruktor const od zwykłego konstruktora?

markovuksanovic
źródło

Odpowiedzi:

79

Konstruktor Const tworzy instancję „kanoniczną”.

Oznacza to, że wszystkie wyrażenia stałe zaczynają być kanonizowane, a później te „kanonizowane” symbole są używane do rozpoznawania równoważności tych stałych.

Kanonizacja:

Proces przekształcania danych, które mają więcej niż jedną możliwą reprezentację, na „standardową” reprezentację kanoniczną. Można to zrobić, aby porównać różne reprezentacje pod względem równoważności, policzyć liczbę różnych struktur danych, poprawić wydajność różnych algorytmów poprzez wyeliminowanie powtarzających się obliczeń lub umożliwić narzucenie sensownej kolejności sortowania.


Oznacza to, że wyrażenia const, takie jak, const Foo(1, 1)mogą reprezentować dowolną użyteczną formę, która jest przydatna do porównania w maszynie wirtualnej.

Maszyna wirtualna musi tylko wziąć pod uwagę typ wartości i argumenty w kolejności, w jakiej występują w tym wyrażeniu const. I oczywiście są one redukowane w celu optymalizacji.

Stałe z tymi samymi wartościami kanonizowanymi:

var foo1 = const Foo(1, 1); // #Foo#int#1#int#1
var foo2 = const Foo(1, 1); // #Foo#int#1#int#1

Stałe z różnymi wartościami kanonizowanymi (ponieważ podpisy się różnią):

var foo3 = const Foo(1, 2); // $Foo$int$1$int$2
var foo4 = const Foo(1, 3); // $Foo$int$1$int$3

var baz1 = const Baz(const Foo(1, 1), "hello"); // $Baz$Foo$int$1$int$1$String$hello
var baz2 = const Baz(const Foo(1, 1), "hello"); // $Baz$Foo$int$1$int$1$String$hello

Stałe nie są odtwarzane za każdym razem. Są kanonizowane w czasie kompilacji i przechowywane w specjalnych tabelach odnośników (gdzie są zaszyfrowane według ich sygnatur kanonicznych), z których są później ponownie wykorzystywane.

PS

Forma #Foo#int#1#int#1używana w tych próbkach jest używana tylko do celów porównawczych i nie jest prawdziwą formą kanonizacji (reprezentacji) w Dart VM;

Ale prawdziwa forma kanonizacji musi być „standardową” reprezentacją kanoniczną.

mezoni
źródło
81

Uważam, że odpowiedź Lasse na blogu Chrisa Stormsa to świetne wyjaśnienie.

Dart Constant Constructors

Mam nadzieję, że nie przeszkadza im to, że kopiuję zawartość.

To jest dobre wyjaśnienie końcowych pól, ale tak naprawdę nie wyjaśnia konstruktorów const. Nic w tych przykładach nie używa, że ​​konstruktory są konstruktorami const. Każda klasa może mieć końcowe pola, konstruktory const lub nie.

Pole w Dart jest w rzeczywistości anonimową lokalizacją pamięci połączoną z automatycznie utworzonym geterem i ustawiaczem, który odczytuje i aktualizuje pamięć, a także może być zainicjowany na liście inicjalizacyjnej konstruktora.

Ostatnie pole jest takie samo, tylko bez metody ustawiającej, więc jedyny sposób na ustawienie jego wartości znajduje się na liście inicjalizatora konstruktora i nie ma możliwości zmiany wartości po tym - stąd „ostateczna”.

Celem konstruktorów const nie jest inicjowanie pól końcowych, każdy konstruktor generujący może to zrobić. Chodzi o to, aby utworzyć wartości stałych czasu kompilacji: obiekty, w których wszystkie wartości pól są znane już w czasie kompilacji, bez wykonywania jakichkolwiek instrukcji.

To nakłada pewne ograniczenia na klasę i konstruktora. Konstruktor const nie może mieć treści (żadne instrukcje nie są wykonywane!), A jego klasa nie może mieć żadnych pól innych niż końcowe (wartość, którą „znamy” w czasie kompilacji, nie może być później zmieniona). Lista inicjalizacyjna musi również inicjalizować pola tylko na inne stałe czasu kompilacji, więc prawa strona jest ograniczona do „wyrażeń stałych czasu kompilacji” [1]. Musi być poprzedzony przedrostkiem „const” - w przeciwnym razie po prostu otrzymasz normalnego konstruktora, który spełni te wymagania. To jest w porządku, po prostu nie jest to konstruktor const.

Aby użyć konstruktora const do faktycznego utworzenia obiektu stałego czasu kompilacji, zamień „new” na „const” w „nowym” -wyrażeniu. Nadal możesz używać "new" z konstruktorem const i nadal będzie tworzył obiekt, ale będzie to po prostu normalny nowy obiekt, a nie stała czasu kompilacji. To znaczy: Konstruktor const może być również używany jako normalny konstruktor do tworzenia obiektów w czasie wykonywania, a także do tworzenia obiektów stałych czasu kompilacji w czasie kompilacji.

A więc jako przykład:

class Point { 
  static final Point ORIGIN = const Point(0, 0); 
  final int x; 
  final int y; 
  const Point(this.x, this.y);
  Point.clone(Point other): x = other.x, y = other.y; //[2] 
}

main() { 
  // Assign compile-time constant to p0. 
  Point p0 = Point.ORIGIN; 
  // Create new point using const constructor. 
  Point p1 = new Point(0, 0); 
  // Create new point using non-const constructor.
  Point p2 = new Point.clone(p0); 
  // Assign (the same) compile-time constant to p3. 
  Point p3 = const Point(0, 0); 
  print(identical(p0, p1)); // false 
  print(identical(p0, p2)); // false 
  print(identical(p0, p3)); // true! 
}

Stałe czasu kompilacji są kanonizowane. Oznacza to, że bez względu na to, ile razy napiszesz „const Point (0,0)”, utworzysz tylko jeden obiekt. Może to być przydatne - ale nie tak bardzo, jak mogłoby się wydawać, ponieważ możesz po prostu utworzyć zmienną const, która będzie przechowywać wartość i zamiast tego użyć zmiennej.

A więc do czego służą stałe czasu kompilacji?

  • Są przydatne w przypadku wyliczeń.
  • W przypadkach przełączników można używać wartości stałych czasu kompilacji.
  • Są używane jako adnotacje.

Stałe czasu kompilacji były ważniejsze, zanim Dart przełączył się na leniwe inicjowanie zmiennych. Wcześniej można było zadeklarować tylko zainicjowaną zmienną globalną, taką jak „var x = foo;” jeśli "foo" było stałą w czasie kompilacji. Bez tego wymagania większość programów można napisać bez używania obiektów stałych

Krótkie podsumowanie: konstruktory Const służą tylko do tworzenia wartości stałych czasu kompilacji.

/ L

[1] Albo tak naprawdę: „Potencjalnie wyrażenia stałe w czasie kompilacji”, ponieważ może również odnosić się do parametrów konstruktora. [2] Więc tak, klasa może mieć jednocześnie konstruktory const i non-const.

Ten temat był również omawiany na https://github.com/dart-lang/sdk/issues/36079 z kilkoma interesującymi komentarzami.

Günter Zöchbauer
źródło
AFAIK const i final pozwalają na generowanie bardziej zoptymalizowanego JS.
Günter Zöchbauer,
2
Są również przydatne dla wartości domyślnych w sygnaturach metod.
Florian Loitsch,
1
Czy ktoś może mi wyjaśnić, jak działa ta linia? Point.clone(Point other): x = other.x, y = other.y;
Daksh Gargas
Która część jest niejasna? To nie wygląda na powiązane zconst
Günter Zöchbauer
3
constto niezła wygrana wydajnościowa dla widżetów Flutter według medium.com/@mehmetf_71205/inheriting-widgets-b7ac56dbbeb1 "Użyj const, aby zbudować swoje widżety Bez const, selektywne przebudowywanie poddrzewa nie następuje. Flutter tworzy nową instancję każdego z nich widget w poddrzewie i wywołuje build () marnując cenne cykle, zwłaszcza jeśli twoje metody budowania są ciężkie. "
David Chandler
8

Bardzo dobrze wyjaśnione szczegółowo, ale dla użytkowników, którzy faktycznie szukają użycia konstruktora const

Służy do zwiększania wydajności Flutter, ponieważ pomaga Flutterowi odbudować tylko widżety, które powinny być aktualizowane.Oznacza, że ​​podczas używania setState () w StateFulWidgets zostaną odbudowane tylko te komponenty, które nie są konstruktorami stałymi

Można to wytłumaczyć na przykładzie->

    class _MyWidgetState extends State<MyWidget> {

  String title = "Title";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: <Widget>[
          const Text("Text 1"),
          const Padding(
            padding: const EdgeInsets.all(8.0),
            child: const Text("Another Text widget"),
          ),
          const Text("Text 3"),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          setState(() => title = 'New Title');
        },
      ),
    );
  }
}

Tak jak w tym przykładzie, tylko tytuł tekstu powinien zostać zmieniony, więc tylko ten widżet powinien zostać przebudowany, więc uczynienie wszystkich innych widżetów konstruktorem const pomoże trzepotać to samo w celu zwiększenia wydajności.

B.shruti
źródło
0

Przykładowe demo, w którym instancja const naprawdę decyduje o ostatecznym polu.
W tym przypadku nie można tego przewidzieć w czasie kompilacji.

import 'dart:async';

class Foo {
  final int i;
  final int j = new DateTime.now().millisecond;
  const Foo(i) : this.i = i ~/ 10;

  toString() => "Foo($i, $j)";
}



void main() {
  var f2 = const Foo(2);
  var f3 = const Foo(3);

  print("f2 == f3 : ${f2 == f3}"); // true
  print("f2 : $f2"); // f2 : Foo(0, 598)
  print("f3 : $f3"); // f3 : Foo(0, 598)

  new Future.value().then((_) {
    var f2i = const Foo(2);
    print("f2 == f2i : ${f2 == f2i}"); // false
    print("f2i : $f2i"); // f2i : Foo(0, 608)
  });
}

Teraz dart to sprawdzi.

Analiza rzutek:

[dart] Nie można zdefiniować konstruktora „const”, ponieważ pole „j” jest zainicjowane wartością zmienną

Błąd w czasie wykonywania:

/main.dart ': błąd: wiersz 5, poz. 17: wyrażenie nie jest poprawną stałą czasu kompilacji final int j = new DateTime.now (). millisecond;

Ticore Shih
źródło