Today, we'll dive into advanced function features — higher-order functions and closures. These are core concepts in Dart's functional programming paradigm, making code more concise and flexible, and are essential techniques in Flutter development.
I. Higher-Order Functions: Functions that Work with Other Functions
A higher-order function is a function that can accept other functions as parameters or return a function as a result. Simply put, it's a function that "operates" on other functions.
1. Functions as Parameters
When functions are passed as parameters, we can abstract common logic, making functions more extensible.
Example: Generic Calculation Framework
// Higher-order function: accepts two numbers and an operation function void calculate(int a, int b, int Function(int, int) operation) { int result = operation(a, b); print("Calculation result: $result"); } // Define specific specific operation functions int add(int x, int y) => x + y; int multiply(int x, int y) => x * y; void main() { // Pass the addition function calculate(3, 4, add); // Output: Calculation result: 7 // Pass the multiplication function (or directly pass an anonymous function) calculate(3, 4, multiply); // Output: Calculation result: 12 calculate(3, 4, (x, y) => x - y); // Output: Calculation result: -1 }
In this example, calculate is a higher-order function. It doesn't care about the specific operation logic, only about executing the passed operation function. This pattern is common in framework design.
2. Functions as Return Values
When functions return other functions, we can dynamically generate functions with specific behaviors.
Example: Generating Custom Calculators
// Higher-order function: returns corresponding operation function based on operator Function getCalculator(String operator) { switch (operator) { case "+": return (int a, int b) => a + b; case "-": return (int a, int b) => a - b; case "*": return (int a, int b) => a * b; case "/": return (int a, int b) => a ~/ b; // Integer division default: return (int a, int b) => 0; } } void main() { // Get an addition calculator var adder = getCalculator("+"); print(adder(5, 3)); // Output: 8 // Get a multiplication calculator var multiplier = getCalculator("*"); print(multiplier(5, 3)); // Output: 15 }
Here, getCalculator dynamically returns different operation functions based on the input operator, achieving the effect of "function on demand".
II. Common Higher-Order Functions: forEach/map/reduce
Dart's collection classes (List, Set, etc.) have many built-in higher-order functions that simplify data processing.
1. forEach: Iterating Over Collections
forEach accepts a function parameter and iterates over each element in the collection (which we touched on in the previous lesson).
void main() { List<String> languages = ['Dart', 'Flutter', 'Java']; // Iterate and print each element languages.forEach((lang) { print("Language: $lang"); }); // Output: // Language: Dart // Language: Flutter // Language: Java }
2. map: Transforming Collection Elements
map accepts a transformation function, converts each element in the collection according to rules, and returns a new iterable (usually converted to a list with toList()).
Example: Data Transformation
void main() { List<int> numbers = [1, 2, 3, 4]; // Convert each number to its square var squared = numbers.map((n) => n * n); print(squared.toList()); // Output: [1, 4, 9, 16] // Convert integer list to string list var numberStrings = numbers.map((n) => "Number: $n"); print( numberStrings.toList(), ); // Output: [Number: 1, Number: 2, Number: 3, Number: 4] }
3. reduce: Aggregating Collection Elements
reduce accepts an aggregation function and combines elements in the collection into a single result (starting with the first element and progressively combining with the next).
Example: Summing and Multiplying
void main() { List<int> numbers = [1, 2, 3, 4, 5]; // Sum: ((((1+2)+3)+4)+5) = 15 int sum = numbers.reduce((value, element) => value + element); print("Sum: $sum"); // Output: Sum: 15 // Product: ((((1*2)*3)*4)*5) = 120 int product = numbers.reduce((value, element) => value * element); print("Product: $product"); // Output: Product: 120 }
4. Chaining: Combining Higher-Order Functions
These higher-order functions can be chained together to implement complex data processing logic:
void main() { List<int> scores = [85, 92, 78, 65, 90]; // Step 1: Filter scores greater than 80 // Step 2: Convert scores to strings like "Excellent: XX" // Step 3: Iterate and print results scores .where((s) => s > 80) // Filter: [85, 92, 90] .map( (s) => "Excellent: $s", ) // Transform: ["Excellent: 85", "Excellent: 92", "Excellent: 90"] .forEach((s) => print(s)); // Iterate and print // Output: // Excellent: 85 // Excellent: 92 // Excellent: 90 }
This chaining style is very concise, avoiding verbose loops and temporary variables.
III. Closures: "Binding" Between Functions and Variables
A closure is a structure formed when an inner function in a nested function references variables from the outer function, and the inner function is returned or passed outside. Simply put, it's a function that "remembers" the environment in which it was created.
1. Basic Form of a Closure
// Outer function Function makeCounter() { int count = 0; // Local variable of outer function // Inner function (closure): references outer count variable int increment() { count++; return count; } return increment; // Return inner function } void main() { // Get closure function var counter = makeCounter(); // Calling multiple times shows count is "remembered" print(counter()); // Output: 1 print(counter()); // Output: 2 print(counter()); // Output: 3 }
In this example, the increment function is a closure. It references the count variable from the outer function makeCounter. Even after makeCounter finishes executing, count isn't destroyed but is "preserved" by the increment function.
2. Core Feature of Closures: Variable Isolation
Each closure maintains its own independent variable environment, without interference:
Function makeCounter() { int count = 0; return () { count++; return count; }; } void main() { var counter1 = makeCounter(); var counter2 = makeCounter(); print(counter1()); // 1 (counter1's count) print(counter1()); // 2 print(counter2()); // 1 (counter2's count, independent of counter1) print(counter2()); // 2 }
counter1 and counter2 are two independent closures with their own count variables that don't affect each other, achieving variable isolation.
3. Practical Use Cases for Closures
- Encapsulating private variables: Dart doesn't have a private keyword, but closures can simulate private variables.
- Caching calculation results: Storing results of expensive calculations to avoid redundant computations.
- Event handling: Preserving context information in callback functions.
Example: Simulating Private Variables
class User { // Implement "private" password variable with closure final _password = (() { String _secret = "123456"; // Not directly accessible from outside return {'get': () => _secret, 'set': (String newPwd) => _secret = newPwd}; })(); String name; User(this.name); // Only access "private" password through methods String getPassword() => _password['get']!(); void setPassword(String newPwd) => _password['set']!(newPwd); } void main() { User user = User("Zhang San"); print(user.getPassword()); // Output: 123456 user.setPassword("abcdef"); print(user.getPassword()); // Output: abcdef // Cannot directly access _secret (compilation error) // print(user._secret); }
IV. Comprehensive Example with Higher-Order Functions and Closures
Let's use a "shopping cart calculation" example to integrate what we've learned:
void main() { // Shopping cart items: [name, price, quantity] List<List<dynamic>> cart = [ ['Apple', 5.99, 2], ['Banana', 3.99, 3], ['Orange', 4.50, 1], ]; // 1. Calculate total price for each item (price * quantity) - using map var itemTotals = cart.map((item) => item[1] * item[2]); // 2. Calculate total cart price - using reduce double total = itemTotals.reduce((sum, price) => sum + price); // 3. Create a price calculator with discount (closure) Function createDiscountCalculator(double discount) { return (double price) => price * (1 - discount); } // 4. Calculate final price with 10% discount var discount90 = createDiscountCalculator(0.1); double finalTotal = discount90(total); print( "Original price: ${total.toStringAsFixed(2)}", ); // Output: Original price: 28.45 print( "After 10% discount: ${finalTotal.toStringAsFixed(2)}", ); // Output: After 10% discount: 25.61 }
In this example, we used map for data transformation, reduce for aggregation, and a closure to create a stateful discount calculator, demonstrating the flexibility of higher-order functions and closures.
Top comments (0)