07 Functions, Classes & Operators How Dart Processes Information

07 Functions, Classes & Operators How Dart Processes Information #

Hello, I’m Chen Hang.

In the previous article, I used a basic “hello world” example to introduce you to Dart’s basic syntax and variable types, and compared them to features in other programming languages. I hope this helped you establish a preliminary impression of Dart.

In reality, although programming languages may differ greatly, they ultimately aim to answer two questions:

  • How to represent information?
  • How to handle information?

In the previous article, we have already addressed how Dart represents information. In today’s article, I will focus on how Dart handles information.

Being a truly object-oriented programming language, Dart abstracts the process of handling information into objects, decomposing functionality in a structured manner. Functions, classes, and operators are the most important means of abstraction.

Next, I will further explain the basic approach of object-oriented design in Dart from the perspective of functions, classes, and operators.

Functions #

A function is a piece of code that is used to independently perform a certain task. As I mentioned in my previous article, in Dart, all types are object types, and functions are also objects with the type Function. This means that functions can be defined as variables and even passed as parameters to another function.

In the following code example, I define a function called isZero that checks whether an integer is zero, and I pass it to another function called printInfo to format and print the result of the check.

bool isZero(int number) { // Check if the integer is zero
  return number == 0; 
}

void printInfo(int number, Function check) { // Use the check function to check if the integer is zero
  print("$number is Zero: ${check(number)}");
}

Function f = isZero;
int x = 10;
int y = 0;
printInfo(x, f);  // Output: 10 is Zero: false
printInfo(y, f);  // Output: 0 is Zero: true

If a function body consists of only one expression, like the isZero and printInfo functions in the above example, we can simplify the function using arrow functions, similar to JavaScript:

bool isZero(int number) => number == 0;

void printInfo(int number, Function check) => print("$number is Zero: ${check(number)}");

Sometimes, a function needs to accept multiple arguments. How can we make the declaration of these kinds of functions more elegant and maintainable, while also reducing the burden on the callers?

In C++ and Java, the approach is to provide function overloading, which means providing functions with the same name but different parameters. However, Dart does not support function overloading because it believes it leads to confusion. Instead, Dart provides optional named parameters and optional positional parameters.

To use these two approaches in function declarations:

  • Add {} to the parameters and specify the parameter values using the syntax paramName: value. This is called optional named parameters.
  • Add [] to the parameters to indicate that they can be ignored. This is called optional positional parameters.

When defining functions using these approaches, we can also set default values for parameters that are not passed. Let me explain the usage of these two approaches with an example of a simple function with two parameters:

// To use optional named parameters, add {} to the parameters in the function definition
void enable1Flags({bool bold, bool hidden}) => print("$bold, $hidden");

// Define default values when declaring optional named parameters
void enable2Flags({bool bold = true, bool hidden = false}) => print("$bold, $hidden");

// Use [] to specify optional positional parameters
void enable3Flags(bool bold, [bool hidden]) => print("$bold, $hidden");

// Define default values when declaring optional positional parameters
void enable4Flags(bool bold, [bool hidden = false]) => print("$bold, $hidden");

// Call the function with optional named parameters
enable1Flags(bold: true, hidden: false); // Output: true, false
enable1Flags(bold: true); // Output: true, null
enable2Flags(bold: false); // Output: false, false

// Call the function with optional positional parameters
enable3Flags(true, false); // Output: true, false
enable3Flags(true); // Output: true, null
enable4Flags(true); // Output: true, false
enable4Flags(true, true); // Output: true, true

I want to emphasize that the usage of optional named parameters is heavily employed in Flutter, so you must remember how to use it.

Classes #

A class is a collection of specific types of data and methods, and it serves as a template for creating objects. Like in other languages, Dart provides built-in support for the concept of classes.

Class Definition and Initialization #

Dart is an object-oriented language, and every object is an instance of a class, all of which inherit from the top-level type Object. In Dart, the declaration of instance variables and methods, as well as class variables and methods, is similar to Java, so I won’t go into too much detail here.

It is worth noting that Dart does not have keywords like public, protected, or private. Instead, you can mark variables and methods as private by prefixing them with an underscore (_). If you don’t use an underscore, they are considered public by default. However, it is important to note that the scope of the underscore is not limited to class-level access, but to library-level access.

Next, let’s take a look at how Dart defines and uses classes with a specific example.

In the Point class, I have defined two member variables: x and y. They are initialized using the constructor shorthand. The member function printInfo prints their values. The class variable factor has a default value of 0 and is printed by the class function printZValue.

class Point {
  num x, y;
  static num factor = 0;

  // Constructor shorthand is equivalent to this.x = x; this.y = y; inside the function body
  Point(this.x, this.y);

  void printInfo() => print('($x, $y)');

  static void printZValue() => print('$factor');
}

var p = Point(100, 200); // The `new` keyword can be omitted
p.printInfo(); // Output: (100, 200)
Point.factor = 10;
Point.printZValue(); // Output: 10

Sometimes, class instantiation requires multiple ways of initialization based on parameters. In addition to optional named parameters and positional parameters, Dart also provides named constructors to make the instantiation process clearer.

Furthermore, similar to C++, Dart supports initialization lists. Before the body of the constructor is executed, you have a chance to assign values to instance variables or even redirect to another constructor.

In the following example, the Point class has two constructors: Point.bottom and Point. Point.bottom redirects the initialization of its member variables to Point, and Point assigns a default value of 0 to z in the initialization list.

class Point {
  num x, y, z;

  Point(this.x, this.y) : z = 0;

  Point.bottom(num x) : this(x, 0);

  void printInfo() => print('($x,$y,$z)');
}

var p = Point.bottom(100);
p.printInfo(); // Output: (100,0,0)

Reuse #

In object-oriented programming languages, there are generally two ways to reuse variables and methods from other classes within a class: inheritance and interface implementation. This is no exception in Dart.

In Dart, you can inherit or implement interfaces from the same parent class:

  • Inheriting from a parent class means the child class is derived from the parent class and automatically inherits the parent class’s member variables and method implementations. The child class can override the constructors and parent class methods as needed.
  • Implementing an interface means the child class only obtains the member variable and method signatures defined in the interface. The child class needs to redefine the member variables and declare and initialize the methods; otherwise, the compiler will throw an error.

Next, let me show you the difference between inheritance and interface implementation in Dart with an example.

The Vector class adds member variables to Point by inheriting from it and overrides the implementation of printInfo. On the other hand, the Coordinate class implements the Point interface and redefines the variable declarations and method implementation of Point.

class Point {
  num x = 0, y = 0;

  void printInfo() => print('($x,$y)');
}

// Vector inherits from Point
class Vector extends Point {
  num z = 0;

  @override
  void printInfo() => print('($x,$y,$z)'); // Overrides the printInfo implementation
}

// Coordinate implements the Point interface
class Coordinate implements Point {
  num x = 0, y = 0; // Member variables need to be redeclared

  void printInfo() => print('($x,$y)'); // Member methods need to be redeclared and implemented
}

var xxx = Vector();
xxx
  ..x = 1
  ..y = 2
  ..z = 3; // Cascade operator; equivalent to xxx.x = 1; xxx.y = 2; xxx.z = 3;
xxx.printInfo(); // Output: (1,2,3)

var yyy = Coordinate();
yyy
  ..x = 1
  ..y = 2; // Cascade operator; equivalent to yyy.x = 1; yyy.y = 2;
yyy.printInfo(); // Output: (1,2)
print(yyy is Point); // true
print(yyy is Coordinate); // true

As you can see, the Coordinate class, when implementing the interface, only obtains an “empty shell” of the Point class and can only be used semantically as the interface Point, without reusing the original implementation of Point. So, is there a way to reuse the corresponding method implementation of Point?

Perhaps you quickly thought of inheriting Point in the Coordinate class to reuse its methods. But what if Coordinate has other parent classes as well? How should we handle that?

In fact, besides inheritance and interface implementation, Dart provides another mechanism called “mixins” to achieve class reuse. Mixins encourage code reuse and can be seen as interfaces with implemented methods. This way, not only can we solve the lack of support for multiple inheritance in Dart, but we can also avoid ambiguity caused by multiple inheritance, such as the diamond problem.

Note: Inheritance ambiguity, also known as the diamond problem, is a challenging issue in programming languages that support multiple inheritance. It occurs when class B and class C inherit from class A, and class D inherits from both class B and class C. If class A has a method overridden in both class B and class C, but class D does not override it, which version of the inherited method should class D have, the one from class B or class C?

To use mixins, simply use the with keyword. Let’s try to modify the implementation of the Coordinate class by removing all variable declarations and method implementations:

class Coordinate with Point {
}

var yyy = Coordinate();
print(yyy is Point); // true
print(yyy is Coordinate); // true

As you can see, by using mixins, a class can use variables and methods from another class without inheritance, just as you might have imagined.

Operators #

Dart and most programming languages have the same operators, so you can perform program code operations in a familiar way. However, Dart has a few additional operators to simplify handling of null values.

  • The ?. operator: Assuming the Point class has a printInfo() method, p is a possibly null instance of Point. Then, the safe code for calling a member method on p can be simplified to p?.printInfo(), which means skip if p is null, to avoid throwing an exception.
  • The ??= operator: If a is null, assign it the value, otherwise skip. We can use a ??= value to achieve this default value assignment statement in Dart.
  • The ?? operator: If a is not null, return the value of a, otherwise return b. In Java or C++, we need to use a ternary expression (a != null)? a : b to achieve this. In Dart, such code can be simplified to a ?? b.

In Dart, everything is an object, including operators, which are part of object member functions.

For system operators, they generally support basic data types and types provided in the standard library. However, for user-defined classes, if you want to support basic operations like comparison and addition/subtraction, you need to define the specific implementation for these operators.

Dart provides an operator overloading mechanism similar to C++, which allows us to not only override methods but also override or define operators.

Next, let’s take a look at an example in the Vector class that customizes the “+” operator and overrides the “==” operator:

class Vector {
  num x, y;
  Vector(this.x, this.y);
  // Custom addition operator to add vectors
  Vector operator +(Vector v) =>  Vector(x + v.x, y + v.y);
  // Override equality operator to check vector equality
  bool operator ==(dynamic v) => x == v.x && y == v.y;
}

final x = Vector(3, 3);
final y = Vector(2, 2);
final z = Vector(1, 1);
print(x == (y + z)); // Outputs true

“operator” is a keyword in Dart and is used together with operators to indicate a class member operator function. When understanding this, we should consider the operator and the operator together as a single member function name.

Summary #

Functions, classes, and operators are the means of abstraction for handling information in Dart. From today’s lesson, you can see that Dart’s object-oriented design incorporates the advantages of other programming languages, making the expression and handling of information both simple and concise, yet powerful.

Through the content of these two articles, I believe you have gained a basic understanding of Dart’s design principles, become familiar with commonly used syntax features in Flutter development, and have the ability to quickly get started with practice.

Next, let’s briefly review today’s content to deepen your memory and understanding.

First, we learned about functions. Functions are also objects that can be defined as variables or parameters. Dart does not support function overloading, but provides optional named parameters and optional parameters to solve the maintainability issue when multiple parameters need to be passed in when declaring functions.

Then, I taught you about classes. Classes provide the ability to abstractly reuse data and functions, and can be reused through inheritance (class inheritance, interface implementation) or non-inheritance (Mixin). Inside a class, Dart provides two initialization methods for member variables, including named constructors and initialization lists.

Finally, it is important to note that operators are also part of object member functions and can be overridden or customized.

Thought Questions #

Finally, please consider the following two questions.

  1. How do you understand inheritance, interface implementation, and mixins? In what scenarios should we use them?
  2. In the scenario of inheritance between a parent class and a subclass, what is the order of execution of the constructors? If both the parent class and the subclass have multiple constructors, how can we ensure the correct invocation of the constructors between the parent class and the subclass from the code level?
class Point {
  num x, y;
  Point() : this.make(0,0);
  Point.left(x) : this.make(x,0);
  Point.right(y) : this.make(0,y);
  Point.make(this.x, this.y);
  void printInfo() => print('($x,$y)');
}

class Vector extends Point{
  num z = 0;
/*5 constructors
  Vector
  Vector.left;
  Vector.middle
  Vector.right
  Vector.make
*/
  @override
  void printInfo() => print('($x,$y,$z)'); //overrided printInfo implementation
}

Please feel free to leave your answers in the comments and let’s discuss together. Thank you for listening, and please share this article with more friends to read together.