Dart, everything is an object, including a function, which means that you can store the function in a variable and pass the function in the application in the same way as passing String, int, or any other object. This is called first-class functions because they are considered equivalent to other types rather than second-class citizens in the language.
Finally, we'll look at closures, which occur when a function is created and uses variables that exist outside its own scope. When you pass this function (stored in a variable) to another part of the application, it is called a closure. This may be a complex topic; It is widely used in JavaScript to simulate features such as getter s and setter s, as well as class privacy features that have been built into the Dart language.
1.Dart function
High quality universal concrete is produced according to the following formula. Each function has inputs and outputs:
- Measure cement volume.
- Measure cement has twice the volume of sand.
- Measure the amount of gravel three times the volume of cement.
- Mix cement and sand to form a mortar mixture.
- Mix mortar mixture with gravel to form a dry concrete mixture.
- Mix water and concrete mixture to form wet concrete.
- Lay concrete, lay concrete before setting.
In these steps, the measure() and mix() functions are reused, accepting the input from the previous step to generate a new output. When I mix two ingredients, such as cement and sand, this gives me a new ingredient (mortar) that I can use elsewhere in the formula. There is also a lay() function, which I only used once. The initial volume of cement depends on the operation; For example, I use a bag as an approximate unit of measurement.
You can use the code in the following listing to represent these functions in Dart. The various categories returned by the function are omitted from the listing, but they are not necessary for this example (you can find them in the source code related to this book). The main() function follows the concrete mixing instruction set and is the first function executed in all Dart applications.
ingredient component; cement; proportion; Concrete concrete
sand; gravel; mortar; lay laying
Ingredient mix(Ingredient item1, Ingredient item2) { return item1 + item2; } Ingredient measureQty(Ingredient ingredient, int numberOfCementBags, int proportion) { return ingredient * (numberOfCementBags * proportion); } void lay(ConcreteMix concreteMix) { // snip – implementation not required } main() { Ingredient cement = new Cement(); cement.bags = 2; print(cement.bags); Ingredient sand = measureQty(new Sand(), cement.bags, 2); Ingredient gravel = measureQty(new Gravel(), cement.bags, 3); Ingredient mortar = mix(cement, sand); Ingredient dryConcrete = mix(mortar, gravel); ConcreteMix wetConcrete = new ConcreteMix(dryConcrete, new Water()); lay(wetConcrete); }
Dart functions are declaratively similar to Java and C #, because they have return types, names, and parameter lists. Unlike JavaScript, they do not require keyword functions to declare that they are functions; Unlike Java and c# different, parameter types and return types are optional as part of DART's optional type system.
Now that some sample functions have been defined, let's look at some other ways to define these functions in Dart, considering the optional types of Dart and the syntax of long and shorthand functions. Chapter 3 briefly introduces Dart's long hand and shorthand function syntax: shorthand syntax allows you to write single line functions that automatically return single line output. The optional type of Dart also means that the return type and parameter type are optional.
You can only use Dart's shorthand function syntax for single line functions, while you can use long function syntax for single line or multi line functions. Shorthand syntax is very useful for writing concise and clear code.
Function return type and parameter type:
Ingredient mix(item1, item2) { ...snip...; return ingredient;} mix(item1, item2) { ...snip...; return ingredient;} void go(...){...snip...;} go(...){...snip...;}
The first line of code explicitly returns an increment object.
The second line of code is equivalent to the following code (the return type is dynamic keyword):
dynamic mix(item1, item2) { ...snip...; return ingredient;}
The third line of code makes it clear that there is no return value.
The fourth line of code has no return statement, which is equivalent to return null; That is, there is a return value of null.
Differences among dynamic, var and object types:
① Dynamic: the base type of all Dart objects. In most cases, it is not used directly. Variables defined by it will turn off type checking, which means dynamix x = 'hal'; x.foo(); The static type check of this code will not report an error, but it will crash at runtime. Because X has no foo () method, it is recommended not to use dynamic directly in programming;
② var: is a keyword, which means "I don't care what the type here is". The system will automatically determine the runtime type;
③ Object: is the base class of Dart object. When you define: object o =xxx; At this time, the system will think o is an object. You can call O's toString() and hashCode() methods because object provides these methods. However, if you try to call o.foo(), the static type check will run with an error.
To sum up, it is not difficult to see that the biggest difference between dynamic and object is static type checking.
measureQty(Ingredient ingredient, int numberOfCementBags, int proportion) { // ...snip... } calculateQty(ingredient, numberOfCementBags, proportion) { // ...snip... } calculateQty(dynamic ingredient, dynamic numberOfCementBags, dynamic proportion) { // ...snip... }
In the first function, the parameter list has an explicit type declaration.
In the second function, the parameter has no type declaration, which is equivalent to the third function.
Pass parameters by reference:
void main(List<String> arguments) { /* Example 1: value transfer */ String str = "abc"; tryChangeOriginStr(str); print("result1: " + str); /* Example 2: pass reference */ var box = Box(); tryChangeOriginClz(box); print("result2: ${box.x}"); /* Example 3: immutable */ var a = ['Apple', 'Orange']; var b = a; // a. B refers to the same memory object, but there is no relationship between a and B! // Lose the original reference and create a new reference a = ['Banana']; // A's assignment has no effect on b. A now refers to a new object, and b still refers to the original memory object print("result3: a=$a,b=$b"); /* Example 4: mutable */ var m = ['Apple', 'Orange']; var n = m; m.clear();// Clear the list referenced by m, while n always references this memory object, that is, the content in n is cleared m.add('Banana');// Since both M and N refer to the same list at this time, the changes are synchronized to m and n print("result4: m=$m,n=$n"); } void tryChangeOriginStr(String str) {// By value str += "XYZ"; } void tryChangeOriginClz(Box box) {// By reference box.x *= 2; } class Box { num x = 8; }
result1: abc result2: 16 result3: a=[Banana],b=[Apple, Orange] result4: m=[Banana],n=[Banana]
var a = 10; This assignment syntax means that a is a reference (ref) to a memory object 10
var a = 10; var b=a; Then both a and B point to the same memory object, that is, reference the same memory object. But there is no connection between a and B.
About mutable and immutable types
mutable type will reduce the number of copies of data, so its efficiency is higher than immutable. However, the immutability of internal data makes it more secure. It can be used as a shared object of multiple threads without considering synchronization. However, the variable type has greater risk because its internal data is variable. Because the internal data is not variable, frequent modifications will produce a large number of temporary copies and waste space.
Not all objects in Dart are mutable, in which Number, String, Bool, etc. are immutable. Each modification generates a new object. Most other objects are mutable.
Ingredient measureQty(ingredient, numberOfCementBags, proportion) { if (ingredient.bags == 0) { ingredient = new Ingredient(); ingredient.bags = numberOfCementBags * proportion; return ingredient; } } main() { var emptyBagOfCement = new Cement(); emptyBagOfCement.bags = 0; var cement = measureQty(emptyBagOfCement, 1, 1);// return new object'ref print(emptyBagOfCement.bags);// original object unmodified }
You lose the reference to the new type when you return, because everything is an object; When you change the reference to the incoming object, all you do in the function is lose the original reference and create a new reference. Code outside the function still has a handle to the original object reference.
Optional location parameters:
The Dart function can have optional parameters with default values. When creating a function, you can specify the parameters that the calling code can provide; However, if the calling code chooses not to execute, the function uses the default value.
measureQty(ingredient, int numberOfCementBags, int proportion) { if (numberOfCementBags == null) numberOfCementBags = 1; if (proportion == null) proportion = 1; return ingredient * (numberOfCementBags * proportion); } main() { measureQty(new Sand(), null, null); measureQty(new Sand(), 1, null); measureQty(new Sand(), null, 1); measureQty(new Sand(), 1,1); }
For the above code, when numberOfCementBags is null, assign 1; When the proportion is null, it is also assigned to 1. However, if there is a default value, you can simplify the program without passing the default value parameter.
It would be better if the calling code could pass only the values it needs to pass, such as ingredients and proportions, rather than the number of unwanted bags. Dart allows us to do this with optional parameters. After all positional parameters are defined, optional parameters must appear in the block at the same time. Optional parameter blocks are defined in square brackets. Like positional parameters, they are a comma separated list. For example, you can change the sample function to support optional parameters, as follows:
measureQty(ingredient, [int numberOfCementBags, int proportion]) { // ... snap... (null judgment) } // The above functions can be called as follows main() { measureQty(new Sand(), 2, 1); measureQty(new Sand(), 2); measureQty(new Sand()); }
Of course, in this code, if no parameter value is provided, the parameter value will still be initialized to null, which means that measureQty() must still check the null value and default it to 1. Fortunately, you can also provide default values in the function declaration of named parameters:
measureQty(ingredient, [int numberOfCementBags = 1, int proportion = 1]) { return ingredient * (numberOfCementBags * proportion); }
Optional named parameters:
An alternative to optional positional parameters is to use optional named parameters. These allow the calling code to specify the parameters to which the values are passed in any order. As mentioned earlier, mandatory parameters rank first, but this time optional parameters are specified between braces. The default values are as follows:
measureQty(ingredient, {int numberOfCementBags:1, int proportion:1}) { return ingredient * (numberOfCementBags * proportion); } // The above functions can be called as follows main() { measureQty(new Sand(), numberOfCementBags: 2, proportion: 1); measureQty(new Sand(), numberOfCementBags: 2); measureQty(new Sand(), proportion: 1); measureQty(new Sand()); // The wrong call is as follows: measureQty(new Sand(), 2, 1);// the optional values must be named }
remember
■ shorthand functions automatically return values created by single line expressions that make up the body of the function.
■ the long handle function should use the return keyword to return a value; Otherwise, null is automatically returned.
■ you can tell the type checker that you do not intend to return a value by using the return type void.
■ information about the type of parameter is optional.
■ after declaring mandatory parameters, optional parameters can be declared as a comma separated list in square brackets.
■ the calling code can use the name:value syntax to reference optional parameters by name.
2. Use the first class function
The term first-order function means that you can store functions in variables and pass them in your application. There is no special syntax for first-order functions. All functions in Dart are first-order functions. To access a function object instead of calling a function, refer to the function name without parentheses, which is usually used to provide parameters to the function. When you do this, you can access the function object.
Consider the progressive mix (Item1, Item2) function earlier in this chapter. You can call it by putting parentheses after the function name and passing the value of the function parameter, such as mix(sand, cement);. You can also refer to it only by name without using parentheses and parameters; In this way, you can get a reference to a function object that can be used like any other value, such as String or int.
After the function object is stored in a variable, you can call the function again with the new reference, as shown in the following code snippet:
var mortar = mix(sand, cement); var mixFunction = mix; var dryConcrete = mixFunction(mortar, gravel); print(mix is Object);// true, the function is an Object object Object print(mix is Function);// true, the Function is of type Function print(dryConcrete is Object);// true, everything is an object print(dryConcrete is Function);// false, which is only a reference to a function object, not a function type
This concept raises an interesting possibility. If you can store functions in variables, do you need to declare functions in the top-level scope first? No, you can declare a function inline (in another function body) and store it in a variable instead of declaring a function in the top-level library scope. In fact, there are three methods to declare functions inline. One method is to declare functions within the scope of the top-level library, as shown in the following figure.
You have used the top-level library scope to declare functions, such as mix1(), which is called library function. The other three function declarations are in the body of another method, called local function, which needs more explanation. They are part of Dart. They seem simple, but like closures, they can be complex.
Local function declaration:
Ingredient combineIngredients(mixFunc, item1, item2) {// The first argument is a function name (that is, a reference to a function object) return mixFunc(item1, item2);// Call function by function name }
Now that you have used function objects stored in variables, let's look at three ways to declare local functions, starting with the most basic method: simple local function declaration.
➊ simple local function declaration
mix2(item1, item2) { return item1 + item2; }
When you declare a simple local function in the scope of another function, you do not need to provide a termination semicolon, because the closing brace provides a terminator, which is the same as declaring a function in the top-level scope. This is important because the other two declaration methods that explicitly assign functions to variables do require a semicolon after the right brace.
A recursive example:
stir(ingredient, stirCount) { print("Stirring $ingredient") if (stirCount < 10) {// If stirCount is less than 10 stirCount ++;// stirCount auto increment stir(ingredient, stirCount);// Call stir() again (recursion) } }
➋ anonymous function declaration
var mix3 = (item1, item2) { return item1 + item2; };// Just like declaring a variable, the last semicolon must be added
Anonymous functions are declared without a function name. Like any other function declaration, you can assign it to a function object variable, pass it directly to another function, or use it as the return type of the declared function. But you can't use it recursively because a function doesn't have its own name in its scope.
() => null; This is a valid anonymous function that does not accept any parameters and returns a null value.
Example: store anonymous functions in a list
main() { List taskList = new List(); taskList.add( (item) => item.pour() ); taskList.add( (item) { item.level(); item.store(); } ); var aggregate = new Aggregate(); foreach(task in taskList) { task(aggregate); } }
You can directly take a complete anonymous function as the parameter of the calling function:
combineIngredients( (item1, item2) => item1 + item2, sand, gravel);
In the following code, the function is called increment instead of an anonymous function:
Ingredient(item) => item.openBag();
The return type is not provided because it considers the function name to be incremental.
This problem can be solved by declaring the third and last method of local functions: function assignment.
➌ assignment declaration of named function
The third way to declare a function is a mixture of the first two versions, that is, to declare a named function and immediately assign it to a variable. As shown in the figure below:
The advantage of this method is that you can declare the return type, and you have a function name within the scope of the function, allowing recursion if necessary. In this example, the function name in the function scope is mixer(), which is only available in the function scope. To reference the function elsewhere, you must use the name mix4.
If you pass the mix4() function as a parameter to the combineIncrements() function, you can override the function to use recursion and provide type information, as shown below.
main() { var mix4 = Ingredient mixer(Ingredient item1, Ingredient item2) { if (item1 is Sand) { return mixer(item2, item1); } else ( return item1 + item2; ) } var sand = new Sand(); var gravel = new Gravel(); combineIngredients(mix4, sand, gravel); }
The name mixer() is essentially one-time. It is only available within the scope of a function and is not valid elsewhere. When you declare a mixer() function directly as an argument to another function, you cannot reference mixer() anywhere but itself.
This example looks almost the same as the simple local function declaration we saw first, but it is slightly different because the function is implicitly assigned to the parameters of the CombineIngCredits() function, as shown in the following figure.
We've studied declaring functions and assigning them to variables and function parameters, but what about Dart's type system? How do you know that the combineincreases () function takes another function as its first argument? Fortunately, Dart allows powerful function types and provides a new keyword typedef.
Define strong function types:
You have seen a Function "is-an" Object and a Function "is-a", so you can use these types, as shown in the following listing.
// The mixfunction parameter is a strong type of a Function Ingredient combineIngredients(Function mixFunc, item1, item2) { return mixFunc(item1, item2); } main() { // Store the Function in a Function strong type called mix Function mix = (item1, item2) { return item1 + item2; } var sand = new Sand(); var gravel = new Gravel(); // When passing mix to the calling function, the type checker will verify whether the first parameter you provide is a function combineIngredients(mix, sand, gravel); }
When you use a function object stored in a variable, you use an instance of a function class. However, not all function instances are the same. The mix() function is different from the measureQty() function, which is different from the lay() function.
You need a way to strongly type the mix() function parameter on combineinglements () to specify that it requires a mix() function and not another.
Dart provides two ways to do this. The first is lighter but more detailed: provide function signature as function parameter definition, as shown in the following figure.
This method is a verbose way to declare that function parameters must have a specific signature. Imagine if 10 functions accept a mix() function;
You need to write the function 10 times. Fortunately, Dart allows you to create custom function types by declaring function signatures using the typedef keyword. Typedef declares that you are defining a function signature, not a function or function object. Typedef can only be used in the top-level scope of the library, not in other functions. The following table shows how to use typedef to define a function signature that can be used to replace the mixFunc parameter declaration on the combineingingradients() parameter list.
typedef Ingredient MixFunc(Ingredient, Ingredient); Ingredient combineIngredients(MixFunc mixFunc, item1, item2) { return mixFunc(item1, item2); }
remember
■ when using a function by name, if there are no parameter parentheses, you will get a reference to its function object.
■ simple local functions declared in a similar way to top-level library scope functions can reference themselves by name, and can make full use of parameters and return type information to provide type information to tools.
■ anonymous functions have no name and cannot use recursion or specify strong return type information, but they do provide useful shorthand for adding functions to the list or passing them as parameters to other functions.
■ you can use named functions instead of anonymous functions to allow recursion and strong return of type information, but its name is only available in its own scope.
■ you can use the typedef keyword to declare a specific function signature so that the type checker can validate the function object.
3. Closures
When a function object references another variable declared outside its own direct scope. Closures are a powerful functional programming concept.
Closures are a special way to use functions. When passing function objects around an application, developers often create them without realizing it. Closures are widely used in JavaScript to simulate various constructs in class based languages, such as getter s, setter s and private properties. The method is to create a function with the sole purpose of returning another function. But Dart supports these constructs locally; Therefore, closures are unlikely to be required when writing new code. However, a large amount of code may be ported from JavaScript to Dart. Dart's closure support is similar to JavaScript, which will help this work.
When you declare a function, it does not execute immediately; It is stored as a function object in a variable in the same way as a string or int is stored in a variable. Similarly, when you declare a function, you can also use other variables previously declared, as shown in the following code snippet:
main() { var cement = new Cement(); mix(item1, item2) { return cement + item1 + item2; } }
Function to mix ingredients, but as shown in the following list, there is also some sticky mud on the shovel. When the getShovel() function returns, the mix() function retains the reference to stickyMud. Even if the getShovel() function has exited, stickyMud will be mixed with the ingredients.
getShovel() { var stickyMud = new Mud(); var mix = (item1, item2) { return stickyMud + item1 + item2; } return mix; } main() { // Call getShovel(), which returns mix(), still containing a reference to stickyMud var mixFunc = getShovel(); var sand = new Sand(); var cement = new Cement(); var muddyMortar = mixFunc(sand, cement); }
remember
■ functions that use variables that are not declared in their own scope may become closures.
■ when a function is passed outside the scope declaring it by passing it to another function or returning from the function declaring it, the function becomes a closure.
summary
This chapter shows you how to declare functions using shorthand syntax and long write syntax. When using shorthand syntax, it also implicitly returns the value of the single line expression that makes up the body of the shorthand function. However, when using long handle syntax, you must explicitly use the return keyword to return the value of the expression.
If no other value is specified, all functions return null values, but you can tell the Dart tool that you do not want to specify a return value by using the void return type.
Functions can be stored in variables, or they can be referenced by accessing the function with an unsupported name. This method provides you with a variable containing a function object that you can pass in your application like other variables. You can return a function object stored in a variable or pass it to another function where it can be called like any other declared function. Function objects and function classes share an "is an" relationship.
To strongly type a function object variable or parameter so that the type checker can validate the code, define a named function signature using the keyword typedef in the library's top-level scope. You can then use the name of the function signature as you would any other type.
We also studied closures, which are created when a function uses variables that are not declared in the function, and the function is passed to another part of the code. You can use closures to use implementation details that the receiving function should or might not know.
Now that you know all the functions, we will introduce Dart's Library and privacy mechanism in the next chapter. This information is important because the names of functions and classes used in the library have a great impact on privacy. These two concepts are closely linked, and you need to understand this topic before you start studying Dart's classes and interfaces.