📜 ⬆️ ⬇️

Characteristic features of the Dart language

Dart was designed to look familiar to programmers in languages ​​such as Java and JavaScript. If you try, you can write on Dart almost the same as on one of them. If you really try - you can even turn it into FORTRAN, but at the same time you will miss many of the unique and cool features of Dart.

This article will help you learn how to write code in the style of Dart. Since the language is still actively developing, many idioms can also change in the future. In some places we have not yet decided what is the best practice (maybe you can help us?) However, here are a few points that you should pay attention to to switch your brains from Java or JavaScript mode to Dart mode.

Constructors


We will begin this article in the same way that objects start their lives - with constructors. Each object will have to be created in the constructor, and its definition is an important point in creating a quality class. Dart has some interesting features.

Automatic field initialization

To get started, get rid of boring reps. Constructors often take arguments and simply assign their values ​​to the fields of the class:
')
class Point { num x, y; Point(num x, num y) { this.x = x; this.y = y; } } 

We had to type x four times just to initialize the field. Sucks! Better to do this:

 class Point { num x, y; Point(this.x, this.y); } 

If in the constructor argument list before the argument name goes this. , the field with this name will be automatically initialized with the argument value. In our example, another small trick is used - if the body of the constructor is empty, you can use it ; instead of {} .

Named Constructors

Like most dynamic languages, Dart does not support overloading. In the case of methods, this is not so scary, because we can just think of another name. Constructors are less fortunate. To ease their plight, Dart allows you to use named constructors:

 class Point { num x, y; Point(this.x, this.y); Point.zero() : x = 0, y = 0; Point.polar(num theta, num radius) { x = Math.cos(theta) * radius; y = Math.sin(theta) * radius; } } 

The class Point has three constructors — a regular one and two named. Here's how to use them:

 var a = new Point(1, 2); var b = new Point.zero(); var c = new Point.polar(Math.PI, 4.0); 

Note that we use new with named constructors, these are not ordinary static methods.

Factory Designers

Sometimes it is useful to use the “factory” design pattern. For example, you need to create an instance of a class, but some flexibility is needed, just to hard-code a constructor call is not enough. Perhaps you want to return the cached instance, if any, or an object of another type.

Dart allows you to do this without changing the code in the place where the object is created. You can create a factory constructor that is invoked just like a normal one. For example:

 class Symbol { final String name; static Map<String, Symbol> _cache; factory Symbol(String name) { if (_cache == null) { _cache = {}; } if (_cache.containsKey(name)) { return _cache[name]; } else { final symbol = new Symbol._internal(name); _cache[name] = symbol; return symbol; } } Symbol._internal(this.name); } 

We defined the Symbol class. A character is about the same as a string, but we want to ensure that at any given time there is only one character with the given name. This makes it possible to safely test symbols for equality, simply by making sure that they point to the same object.

Before defining a default constructor (nameless) there is a keyword factory . When it is called, a new object is not created ( this missing from the factory constructor). Instead, we need to explicitly create and return an object. In this example, we first check if there is a character with the same name in the cache, and return it if there is one.

It's cool that all this is transparent to the calling code:

 var a = new Symbol('something'); var b = new Symbol('something'); 

The second call does not actually create a new character, but returns an existing one. This is convenient, because if at first we didn’t need a factory constructor, and then it turned out that we did, we wouldn’t have to change new somewhere in the code to call a static factory method.

Functions


As in most modern languages, Dart functions are first-class objects, with closures and a lightweight version of the syntax. Any function is an object, and you can do anything with it. We make extensive use of functions for event handlers.

There are three ways to create functions in Dart. The first is named functions:

 void sayGreeting(String salutation, String name) { final greeting = '$salutation $name'; print(greeting); } 

It looks like a regular function declaration in C or a method in Java or JavaScript. Unlike C and C ++, function declarations can be nested. The second method is anonymous functions:

 window.on.click.add((event) { print('You clicked the window.'); }) 

In this example, we pass an anonymous function to the add() method as an event handler. Finally, for small functions consisting of a single expression, there is a simplified syntax:

 var items = [1, 2, 3, 4, 5]; var odd = items.filter((i) => i % 2 == 1); print(odd); // [1, 3, 5] 

The argument in brackets, followed by the arrow ( => ) and the expression, create a function that takes this argument and returns the result of evaluating the expression.

In practice, we prefer to use the switch notation wherever possible, because of its brevity (not to the detriment of expressiveness). We often use anonymous functions for event handlers and callbacks. Named functions are rarely used.

There is another trick in Dart (this is one of my favorite language chips) - you can use => to define class members. Of course, you can do it like this:

 class Rectangle { num width, height; bool contains(num x, num y) { return (x < width) && (y < height); } num area() { return width * height; } } 

But why, if it is possible so:

 class Rectangle { num width, height; bool contains(num x, num y) => (x < width) && (y < height); num area() => width * height; } 

We find a great arrow notation for defining simple getters / setters and other one-line functions.

Fields, getters and setters


To work with properties Dart uses the standard syntax of the form object.someProperty . In Dart, you can define methods that will look like a call to a class field, but still execute arbitrary code. As in other languages, such methods are called getters and setters:

 class Rectangle { num left, top, width, height; num get right() => left + width; set right(num value) => left = value - width; num get bottom() => top + height; set bottom(num value) => top = value - height; Rectangle(this.left, this.top, this.width, this.height); } 


We have a Rectangle class with four “real” properties — left , top , width , and height and two logical properties in the form of getters and setters — right and bottom . When using the class, there is no visible difference between natural fields and getters and setters:

 var rect = new Rectangle(3, 4, 20, 15); print(rect.left); print(rect.bottom); rect.top = 6; rect.right = 12; 

Erasing the border between fields and getters / setters is one of the fundamental properties of a language. It is best to think of the fields as a set of “magic” getters and setters. From this it follows that you can completely override the inherited getter with a natural field and vice versa. If the interface requires a getter, in the implementation you can simply specify a field with the same name and type. If the field is mutable (not final ), you can write the setter required by the interface.

In practice, this means that there is no need to carefully isolate class fields with a bunch of getters and setters, as in Java or C #. Feel free to announce public properties. If you want to prevent them from being modified, use the final keyword.

Later, if it becomes necessary to do a validation or something else like this, you can always replace this field with a getter and a setter. For example, we want our Rectangle class to always have a non-negative size:

 class Rectangle { num left, top; num _width, _height; num get width() => _width; set width(num value) { if (value < 0) throw 'Width cannot be negative.'; _width = value; } num get height() => _height; set height(num value) { if (value < 0) throw 'Height cannot be negative.'; _height = value; } num get right() => left + width; set right(num value) => left = value - width; num get bottom() => top + height; set bottom(num value) => top = value - height; Rectangle(this.left, this.top, this._width, this._height); } 

We added validation to the class without having to change the code in any other place.

Top level definitions


Dart is a pure object-oriented language. All that can be placed in a variable is an object (no mutable “primitives”), and each object is an instance of a class. However, this is not a “dogmatic” OOP — it is not necessary to put everything inside the classes. Instead, you can define variables, functions, and even getters and setters at the top level.

 num abs(num value) => value < 0 ? -value : value; final TWO_PI = Math.PI * 2.0; int get today() { final date = new DateTime.now(); return date.day; } 

Even in languages ​​that do not require to place all certain classes or objects, such as JavaScript, it is customary to do so in order to avoid name conflicts: global definitions in different places may collide. To cope with this, Dart has a library system that allows you to import definitions from other files, adding prefixes to them to avoid ambiguity. So there is no need to hide definitions inside classes.

We are still exploring how this feature can affect the way libraries are written. Most of our code places definitions inside classes, such as Math . It is difficult to say that this is an ingrained habit from other languages, or a programming practice that is also useful for Dart. In this area, we really need feedback from other developers.

We have several examples of using top-level definitions. First of all - this is main() . When working with the DOM, the “variables” document and window are getters defined at the top level.

Strings and interpolation


Dart has several kinds of string literals. You can use double and single quotes, as well as triple quotes for multi-line literals:

 'I am a "string"' "I'm one too" '''I'm on multiple lines ''' """ As am I """ 

To combine multiple lines into one, you can use concatenation:

 var name = 'Fred'; var salutation = 'Hi'; var greeting = salutation + ', ' + name; 

But interpolation will be cleaner and faster:

 var name = 'Fred'; var salutation = 'Hi'; var greeting = '$salutation, $name'; 

In place of the dollar sign ( $ ), followed by the variable name, the value of the variable will be substituted (if it is not a string, the toString() method will be called). Expressions can be placed inside curly brackets:

 var r = 2; print('The area of a circle with radius $r is ${Math.PI * r * r}'); 

Operators


Dart uses the same operators, with the same priorities as C, Java, and other similar languages. They will behave as you expect. However, the internal implementation has its own characteristics. In Dart, an expression with a 1 + 2 operator is just a syntactic sugar for calling a method. From the point of view of language, this example looks like 1.+(2) .

This means that you can overload most operators by creating your own types. For example, the Vector class:

 class Vector { num x, y; Vector(this.x, this.y); operator +(Vector other) => new Vector(x + other.x, y + other.y); } 


Now we can add vectors using familiar syntax:

 var position = new Vector(3, 4); var velocity = new Vector(1, 2); var newPosition = position + velocity; 


However, you should not abuse this opportunity. We give you car keys in the hope that you will not ram the pillars.

In practice, if the types you define are used by operators in the real world, this is a good indication that it is worth redefining standard operators for them: complex numbers, vectors, matrices, etc. In all other cases, operators should not be overloaded. Types with overloaded operators usually must be immutable.

It is worth noting that since operators are calls to methods, asymmetry is inherent in them. The method search is always done for the left argument. So when you write a + b , the meaning of the operation depends on the type a .

Equality


This set of operators should be given special attention. Dart has two pairs of equality operators: == and != And === and !== . It looks familiar to JavaScript programmers, but here they work a little differently.

== and != serve to check for equivalence. 99% of the time you will use them. Unlike JavaScript, they do not make any implicit conversions, so they will behave more predictably. Do not be afraid to use them! Unlike Java, they work with any types for which an equivalence relation is defined. No more someString.equals("something") .

You can overload == for your types, if that makes sense. There is no need to overload != , Dart will automatically output it from == .

The second pair of operators, === and !== , is used to check for identity. a === b returns true only if a and b are the same object in memory. You rarely have to use them in practice. By default, == relies on === if the type does not define an equivalence relation, so === will be needed in a single case — to bypass the user-defined == .

Source: https://habr.com/ru/post/130120/


All Articles