ZetCode

Dart 中的 Function.apply

最后修改于 2025 年 5 月 25 日

本教程将探讨 Dart 中的 Function.apply,这是一个用于动态函数调用的强大功能。Function.apply 允许使用作为列表提供的参数来调用函数和构造函数,从而实现元编程模式和灵活的 API 设计。

Function.apply 概述

Function.apply 是一个静态方法,它使用位置参数列表和可选的命名参数映射来调用函数。它提供了一种动态调用函数的方式,这对于实现插件、命令调度程序或其他在编译时未知的确切调用函数模式特别有用。

该方法签名是

static apply(Function function, List positionalArguments, [Map<Symbol, dynamic> namedArguments])

它返回被调用函数返回的任何内容,并可以抛出被调用函数抛出的任何异常。

参数 类型 描述
function 函数 要调用的函数
positionalArguments List 位置参数列表
namedArguments Map<Symbol, dynamic> 可选的命名参数映射

Function.apply 是 Dart 有限的反射功能的一部分。虽然 Dart 没有像某些其他语言那样完整的运行时反射,但 Function.apply 提供了足够的功能来满足许多动态调用场景,而不会影响 tree-shaking 和优化。

Function.apply 的基本用法

本示例演示了 Function.apply 与常规函数、构造函数和方法调用的基本用法。我们将使用提供的 Point 类和函数来展示不同的调用模式。

basic_apply.dart
class Point {
  final int x, y, z;

  Point(this.x, this.y, this.z);

  @override
  String toString() => 'Point($x, $y, $z)';
}

int sum(int a, int b, int c) => a + b + c;

void printNames(String first, String second, String third) {
  print('Names: $first, $second, $third');
}

void main() {
  var numbers = [1, 2, 3];

  // Using Function.apply for sum function
  var result = Function.apply(sum, numbers);
  print('Sum: $result'); // Output: Sum: 6

  // Using Function.apply for constructor invocation
  var coords = [4, 5, 6];
  var point = Function.apply(Point.new, coords);
  print('Point: $point'); // Output: Point(4, 5, 6)

  // Using Function.apply for printNames function
  List<String>? names = ['Alice', 'Bob', 'Charlie'];
  if (names != null && names.length == 3) {
    Function.apply(printNames, names);
  } else {
    print('Names list is invalid.');
  }

  // Using Function.apply with dynamically retrieved values
  var userData = getUserCoordinates();
  var userPoint = Function.apply(Point.new, userData);
  print('User point: $userPoint'); // Output: User point: Point(10, 20, 30)
}

List<int> getUserCoordinates() => [10, 20, 30];

该示例展示了 Function.apply 的四个关键用途:调用常规函数(sum)、调用构造函数(Point.new)、调用 void 函数(printNames)以及使用动态检索的值创建对象。每个示例都展示了如何运行时构建参数列表。

请注意使用 Point.new 进行的构造函数调用 - 此语法将构造函数引用为可调用函数对象。参数计数必须与函数的参数计数匹配,否则将抛出异常。

$ dart run basic_apply.dart
Sum: 6
Point: Point(4, 5, 6)
Names: Alice, Bob, Charlie
User point: Point(10, 20, 30)

Function.apply 的命名参数

Function.apply 还支持通过 Symbol 键到值的映射来传递命名参数。本示例演示了如何动态使用命名参数。

named_arguments.dart
class Settings {
  final String theme;
  final bool darkMode;
  final int fontSize;

  Settings({required this.theme, this.darkMode = false, this.fontSize = 14});

  @override
  String toString() => 'Settings(theme: $theme, darkMode: $darkMode, fontSize: $fontSize)';
}

void configureApp({String? theme, bool? notifications, int? timeout}) {
  print('Configuring with: theme=$theme, notifications=$notifications, timeout=$timeout');
}

void main() {
  // Using named arguments with a constructor
  var settingsArgs = {
    #theme: 'dark',
    #darkMode: true,
    #fontSize: 16,
  };
  var settings = Function.apply(Settings.new, [], settingsArgs);
  print(settings);

  // Using named arguments with a function
  var configArgs = {
    #theme: 'light',
    #notifications: true,
  };
  Function.apply(configureApp, [], configArgs);

  // Partial arguments with defaults
  var minimalArgs = {#theme: 'blue'};
  var minimalSettings = Function.apply(Settings.new, [], minimalArgs);
  print(minimalSettings);
}

命名参数使用映射传递,其中键是 Symbol(使用 # 语法创建),值是参数值。该示例展示了带有命名参数的构造函数和函数调用,包括省略某些可选参数的情况。

使用命名参数时,参数映射键必须与函数参数名称(作为 Symbols)完全匹配。当仅使用命名参数时,位置参数列表应为空。

$ dart run named_arguments.dart
Settings(theme: dark, darkMode: true, fontSize: 16)
Configuring with: theme=light, notifications=true, timeout=null
Settings(theme: blue, darkMode: false, fontSize: 14)

错误处理和验证

在使用 Function.apply 时,处理潜在错误非常重要。本示例演示了常见的错误场景以及如何在调用前验证参数。

error_handling.dart
int multiply(int a, int b) => a * b;

class Product {
  final String id;
  final String name;
  
  Product(this.id, this.name);
  
  @override
  String toString() => 'Product(id: $id, name: $name)';
}

void main() {
  // Argument count mismatch
  try {
    Function.apply(multiply, [4]); // Missing argument
  } catch (e) {
    print('Error: $e'); // Invalid argument count
  }

  // Type mismatch
  try {
    Function.apply(multiply, ['a', 'b']); // Wrong types
  } catch (e) {
    print('Error: $e'); // Type error
  }

  // Safe invocation with validation
  var productData = getProductData();
  if (productData is List && productData.length == 2) {
    var product = Function.apply(Product.new, productData);
    print(product);
  } else {
    print('Invalid product data');
  }

  // Handling null values
  List<String>? names = getNames();
  if (names != null && names.length >= 2) {
    Function.apply(printNames, names.take(3).toList());
  } else {
    print('No valid names available');
  }
}

void printNames(String name1, String name2, [String? name3]) {
  print('Names: $name1, $name2${name3 != null ? ', $name3' : ''}');
}

dynamic getProductData() => ['123', 'Dart Book'];
List<String>? getNames() => ['Alice', 'Bob'];

该示例显示了几个失败案例:参数计数不匹配、类型不匹配和空安全问题。然后,它演示了在调用 Function.apply 之前验证参数的防御性编程模式。

当参数列表来自外部源时,请始终验证它们。is 运算符和空检查有助于在调用前确保类型安全。在处理不受信任的输入时,请考虑将 Function.apply 包装在 try-catch 块中。

$ dart run error_handling.dart
Error: NoSuchMethodError: Closure call with mismatched arguments: function 'multiply'
Receiver: Closure: (int, int) => int from Function 'multiply': static.
Tried calling: multiply(4)
Found: multiply(int, int) => int
Error: type 'String' is not a subtype of type 'int' of 'a'
Product(id: 123, name: Dart Book)
Names: Alice, Bob

动态函数选择

Function.apply 在与动态函数选择结合使用时表现出色。本示例展示了如何实现一个简单的命令模式,在该模式中,要在运行时确定要调用的函数。

dynamic_selection.dart
class Calculator {
  static double add(List<double> args) => args.reduce((a, b) => a + b);
  static double multiply(List<double> args) => args.reduce((a, b) => a * b);
  static double average(List<double> args) => add(args) / args.length;
}

void main() {
  var operations = {
    'sum': Calculator.add,
    'product': Calculator.multiply,
    'mean': Calculator.average,
  };

  var command = 'sum'; // Could come from user input
  var numbers = [1.5, 2.5, 3.5];

  if (operations.containsKey(command)) {
    var result = Function.apply(operations[command]!, [numbers]);
    print('Result of $command: $result');
  } else {
    print('Unknown operation: $command');
  }

  // More complex example with validation
  var userInput = getUserInput();
  processCommand(userInput['command'], userInput['args']);
}

void processCommand(String? command, List<double>? args) {
  var operations = {
    'sum': Calculator.add,
    'product': Calculator.multiply,
    'mean': Calculator.average,
  };

  if (command == null || !operations.containsKey(command)) {
    print('Invalid command');
    return;
  }

  if (args == null || args.isEmpty) {
    print('No arguments provided');
    return;
  }

  try {
    var result = Function.apply(operations[command]!, [args]);
    print('$command result: $result');
  } catch (e) {
    print('Error processing command: $e');
  }
}

Map<String, dynamic> getUserInput() => {
  'command': 'product',
  'args': [2.0, 3.0, 4.0]
};

这种模式对于命令处理器、计算器或插件系统很有用,在这些系统中,要执行的操作直到运行时才知道。该示例展示了具有输入验证的简单和更健壮的实现。

函数映射(operations)充当可用操作的注册表。Function.apply 调用根据命令名称动态选择和执行适当的函数。这种方法可以轻松添加新操作,而无需修改命令处理逻辑。

$ dart run dynamic_selection.dart
Result of sum: 7.5
product result: 24.0

实际用例:JSON 处理

本示例演示了 Function.apply 在从 JSON 数据动态创建对象方面的实际应用。我们将根据 JSON 中的类型信息创建对象。

json_processing.dart
class User {
  final String id;
  final String name;
  final int age;

  User(this.id, this.name, this.age);

  @override
  String toString() => 'User(id: $id, name: $name, age: $age)';
}

class Product {
  final String sku;
  final String description;
  final double price;

  Product(this.sku, this.description, this.price);

  @override
  String toString() => 'Product(sku: $sku, description: 
        $description, price: $price)';
}

void main() {
  var jsonData = {
    'type': 'user',
    'data': ['u123', 'Alice', 30]
  };

  var object = createFromJson(jsonData);
  print(object);

  jsonData = {
    'type': 'product',
    'data': ['p456', 'Dart Programming Book', 39.99]
  };

  object = createFromJson(jsonData);
  print(object);
}

dynamic createFromJson(Map<String, dynamic> json) {
  var constructors = {
    'user': User.new,
    'product': Product.new,
  };

  var type = json['type'];
  var data = json['data'];

  if (type == null || !constructors.containsKey(type)) {
    throw ArgumentError('Unknown type: $type');
  }

  if (data is! List) {
    throw ArgumentError('Data must be a list');
  }

  try {
    return Function.apply(constructors[type]!, data);
  } catch (e) {
    throw ArgumentError('Failed to create $type: $e');
  }
}

当反序列化 JSON 数据(其中对象类型在数据本身中指定)时,此模式很有用。createFromJson 函数使用构造函数注册表和 Function.apply 来根据 JSON 内容创建适当的对象。

该示例显示了对无效类型规范或数据格式的稳健错误处理。这种方法可以扩展以支持更复杂的对象图或额外的验证规则。

$ dart run json_processing.dart
User(id: u123, name: Alice, age: 30)
Product(sku: p456, description: Dart Programming Book, price: 39.99)

来源

Dart Function.apply API
Dart Functions 文档
Dart Constructors 文档

Function.apply 是 Dart 中动态调用的强大工具。虽然应谨慎使用它(因为它会绕过一些静态类型检查),但它能够实现插件系统、命令处理器和动态对象创建等灵活模式。当与适当的验证和错误处理结合使用时,它可以显著增强 Dart 应用程序的灵活性。

作者

我叫 Jan Bodnar,我是一名充满热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面有十多年的经验。

列出 所有 Dart 教程