مطابقة الأنماط بعمق (Dart 3)
ما هي مطابقة الأنماط؟
مطابقة الأنماط، التي قُدمت في Dart 3.0، هي آلية قوية للتحقق مما إذا كانت القيمة لها شكل معين واستخراج البيانات منها في خطوة واحدة. بدلاً من كتابة سلاسل عبارات if/else if مع فحوصات الأنواع والتحويلات، تصف الشكل الذي تتوقعه وتقوم Dart بالمطابقة والتحقق والتفكيك نيابة عنك.
تعمل مطابقة الأنماط في ثلاثة سياقات: عبارات وتعبيرات switch، وعبارات if-case، وإعلانات المتغيرات. مع السجلات (من الدرس السابق)، تُحوّل مطابقة الأنماط طريقة كتابتك للمنطق الشرطي في Dart.
مطابقة الأنماط مقابل النهج التقليدي
// النهج التقليدي: فحوصات وتحويلات أنواع يدوية
void describeOld(Object value) {
if (value is String) {
print('String of length ${value.length}');
} else if (value is int && value > 0) {
print('Positive integer: $value');
} else if (value is List<int> && value.length == 2) {
print('Pair: (${value[0]}, ${value[1]})');
} else {
print('Something else: $value');
}
}
// نهج مطابقة الأنماط: تصريحي وموجز
void describeNew(Object value) {
switch (value) {
case String s:
print('String of length ${s.length}');
case int n when n > 0:
print('Positive integer: $n');
case [int a, int b]:
print('Pair: ($a, $b)');
default:
print('Something else: $value');
}
}
أنماط الثوابت
أبسط نوع نمط يطابق قيمة ثابتة محددة. هذا ما كانت تفعله عبارات switch التقليدية بالفعل، لكنها الآن تتكامل مع نظام الأنماط الكامل.
أنماط الثوابت
void handleStatusCode(int code) {
switch (code) {
case 200:
print('OK');
case 301:
print('Moved Permanently');
case 404:
print('Not Found');
case 500:
print('Internal Server Error');
default:
print('Unknown status: $code');
}
}
// الثوابت تعمل أيضاً مع النصوص والقيم المنطقية والتعدادات
void handleCommand(String cmd) {
switch (cmd) {
case 'start':
print('Starting...');
case 'stop':
print('Stopping...');
case 'restart':
print('Restarting...');
case '':
print('Empty command');
default:
print('Unknown command: $cmd');
}
}
أنماط المتغيرات
نمط المتغير يطابق أي قيمة ويربطها بمتغير جديد. يمكنك استخدام تعليقات الأنواع لإضافة فحص نوع.
أنماط المتغيرات والمتغيرات المنمطة
void process(Object value) {
switch (value) {
// نمط متغير منمط: يطابق إذا كانت القيمة String، يربط بـ s
case String s:
print('Got string: "$s" (length: ${s.length})');
// نمط متغير منمط: يطابق إذا كانت القيمة int، يربط بـ n
case int n:
print('Got integer: $n (is even: ${n.isEven})');
// نمط متغير منمط مع double
case double d:
print('Got double: ${d.toStringAsFixed(2)}');
// نمط متغير منمط مع bool
case bool b:
print('Got boolean: $b');
// نمط متغير غير منمط: يطابق أي شيء (التقاط الكل)
case var other:
print('Got something else: $other (${other.runtimeType})');
}
}
void main() {
process('Hello'); // Got string: "Hello" (length: 5)
process(42); // Got integer: 42 (is even: true)
process(3.14); // Got double: 3.14
process(true); // Got boolean: true
process([1, 2]); // Got something else: [1, 2] (List<int>)
}
أنماط البدل
نمط البدل _ يطابق أي قيمة لكن لا يربطها بمتغير. استخدمه عندما تحتاج لمطابقة موضع لكن لا تهتم بالقيمة.
استخدام نمط البدل
void main() {
final list = [1, 2, 3];
// مطابقة قائمة من 3 عناصر لكن الاهتمام فقط بالعنصر الأوسط
switch (list) {
case [_, int middle, _]:
print('Middle element: $middle'); // Middle element: 2
}
// في تفكيك السجلات
final record = ('ignored', 42, true);
final (_, value, _) = record;
print(value); // 42
// بدل منمط: مطابقة النوع بدون ربط
final Object obj = 'Hello';
switch (obj) {
case int _:
print('It is an int (but I do not need the value)');
case String _:
print('It is a String (but I do not need the value)');
// يطبع: It is a String (but I do not need the value)
}
}
أنماط القوائم
أنماط القوائم تطابق القوائم حسب طولها وأنماط عناصرها. يمكنها تفكيك العناصر حسب الموضع واستخدام أنماط البقية (...) لمطابقة قوائم متغيرة الطول.
أنماط القوائم
void describeList(List<int> list) {
switch (list) {
case []:
print('Empty list');
case [int single]:
print('Single element: $single');
case [int first, int second]:
print('Two elements: $first, $second');
case [int first, ...]:
print('Starts with $first, has ${list.length} elements total');
}
}
void main() {
describeList([]); // Empty list
describeList([42]); // Single element: 42
describeList([1, 2]); // Two elements: 1, 2
describeList([1, 2, 3]); // Starts with 1, has 3 elements total
// نمط البقية يمكنه التقاط العناصر المتبقية
final numbers = [1, 2, 3, 4, 5];
switch (numbers) {
case [int first, int second, ...var rest]:
print('First: $first, Second: $second');
print('Rest: $rest'); // Rest: [3, 4, 5]
}
// نمط البقية في البداية
switch (numbers) {
case [...var init, int last]:
print('Init: $init, Last: $last');
// Init: [1, 2, 3, 4], Last: 5
}
}
أنماط الخرائط
أنماط الخرائط تطابق الخرائط بالتحقق من مفاتيح محددة ومطابقة قيمها مع أنماط فرعية. على عكس أنماط القوائم، لا تتطلب أنماط الخرائط أن تحتوي الخريطة على المفاتيح المحددة فقط -- المفاتيح الإضافية تُتجاهل.
أنماط الخرائط
void processConfig(Map<String, dynamic> config) {
switch (config) {
case {'type': 'database', 'host': String host, 'port': int port}:
print('Database at $host:$port');
case {'type': 'cache', 'provider': String provider}:
print('Cache provider: $provider');
case {'type': String type}:
print('Unknown config type: $type');
default:
print('Invalid config: missing type');
}
}
void main() {
processConfig({'type': 'database', 'host': 'localhost', 'port': 5432});
// Database at localhost:5432
processConfig({'type': 'cache', 'provider': 'redis', 'ttl': 3600});
// Cache provider: redis (المفتاح الإضافي 'ttl' يُتجاهل)
processConfig({'type': 'queue'});
// Unknown config type: queue
processConfig({'name': 'test'});
// Invalid config: missing type
}
أنماط السجلات
أنماط السجلات تفكك السجلات بمطابقة الحقول الموضعية والمسماة، مما يتيح تركيبات قوية مع أنواع الأنماط الأخرى.
أنماط السجلات
void main() {
// نمط سجل موضعي
final pair = (42, 'hello');
switch (pair) {
case (int n, String s) when n > 0:
print('Positive $n with string "$s"');
case (0, String s):
print('Zero with "$s"');
case (int n, _):
print('Negative $n with something');
}
// نمط سجل مسمى
final user = (name: 'Edrees', age: 28, role: 'admin');
switch (user) {
case (name: String name, age: int age, role: 'admin'):
print('Admin $name (age $age)');
case (name: String name, age: int age, role: 'user'):
print('User $name (age $age)');
case (name: String name, :var age, :var role):
print('Other: $name, $age, $role');
}
// الإخراج: Admin Edrees (age 28)
}
أنماط الكائنات
أنماط الكائنات تطابق مثيلات الفئات بالتحقق من نوع وقت التشغيل ثم مطابقة خصائص الكائن (المُعيدات) مع أنماط فرعية.
أنماط الكائنات
class Point {
final double x;
final double y;
const Point(this.x, this.y);
}
class Circle {
final Point center;
final double radius;
const Circle(this.center, this.radius);
}
class Rectangle {
final Point topLeft;
final double width;
final double height;
const Rectangle(this.topLeft, this.width, this.height);
}
String describeShape(Object shape) {
return switch (shape) {
Circle(center: Point(x: 0, y: 0), radius: var r) =>
'Circle at origin with radius $r',
Circle(center: var c, radius: var r) =>
'Circle at (${c.x}, ${c.y}) with radius $r',
Rectangle(width: var w, height: var h) when w == h =>
'Square with side $w',
Rectangle(topLeft: var tl, width: var w, height: var h) =>
'Rectangle at (${tl.x}, ${tl.y}): ${w}x$h',
_ => 'Unknown shape',
};
}
void main() {
print(describeShape(Circle(Point(0, 0), 5)));
// Circle at origin with radius 5
print(describeShape(Circle(Point(3, 4), 10)));
// Circle at (3.0, 4.0) with radius 10
print(describeShape(Rectangle(Point(1, 1), 5, 5)));
// Square with side 5.0
print(describeShape(Rectangle(Point(0, 0), 10, 20)));
// Rectangle at (0.0, 0.0): 10.0x20.0
}
تعبيرات Switch
يقدم Dart 3 تعبيرات switch -- طريقة موجزة لإرجاع قيمة من مطابقة الأنماط. على عكس عبارات switch، تستخدم تعبيرات switch => بدلاً من :، وتفصل الحالات بفواصل، وتنتج قيمة.
تعبيرات Switch
// تعبير switch يُرجع قيمة
String httpStatus(int code) => switch (code) {
200 => 'OK',
301 => 'Moved Permanently',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
500 => 'Internal Server Error',
_ => 'Unknown ($code)',
};
// استخدام في تعيين المتغيرات
void main() {
final status = httpStatus(404);
print(status); // Not Found
// تعبير switch مضمن
final dayType = switch (DateTime.now().weekday) {
DateTime.saturday || DateTime.sunday => 'Weekend',
_ => 'Weekday',
};
print(dayType);
// تعبير switch مع السجلات
final point = (3, 4);
final quadrant = switch (point) {
(int x, int y) when x > 0 && y > 0 => 'Q1',
(int x, int y) when x < 0 && y > 0 => 'Q2',
(int x, int y) when x < 0 && y < 0 => 'Q3',
(int x, int y) when x > 0 && y < 0 => 'Q4',
_ => 'On axis',
};
print('($point) is in $quadrant'); // ((3, 4)) is in Q1
}
عبارات if-case
بنية if-case تتيح لك استخدام مطابقة نمط واحدة كشرط. مثالية عندما تحتاج فقط للتحقق من نمط واحد بدلاً من كتابة switch كامل.
عبارات if-case
void main() {
// if-case مع فحص نوع وتفكيك
final Object value = [1, 2, 3];
if (value case [int first, ...var rest]) {
print('First: $first, Rest: $rest');
// First: 1, Rest: [2, 3]
}
// if-case مع نمط خريطة (رائع لـ JSON)
final json = {'name': 'Edrees', 'age': 28, 'city': 'Riyadh'};
if (json case {'name': String name, 'age': int age}) {
print('$name is $age years old');
// Edrees is 28 years old
}
// if-case مع شرط حارس
final score = 85;
if (score case int s when s >= 90) {
print('Excellent!');
} else if (score case int s when s >= 80) {
print('Very good!'); // Very good!
} else {
print('Keep trying!');
}
// معالجة القيم القابلة للإلغاء مع if-case
final String? maybeName = getMaybeName();
if (maybeName case String name) {
print('Got name: $name');
} else {
print('Name is null');
}
}
String? getMaybeName() => 'Ahmed';
شروط الحراسة (when)
الكلمة المفتاحية when تضيف شرطاً منطقياً للنمط. النمط يطابق فقط إذا نجحت المطابقة الهيكلية و شرط الحراسة صحيح. المتغيرات المربوطة في النمط متاحة في الحارس.
شروط الحراسة
String classifyAge(int age) => switch (age) {
int a when a < 0 => 'Invalid age',
int a when a < 13 => 'Child',
int a when a < 18 => 'Teenager',
int a when a < 65 => 'Adult',
_ => 'Senior',
};
String classifyTriangle(double a, double b, double c) {
// ترتيب الأضلاع لمقارنة أسهل
final sides = [a, b, c]..sort();
final [s1, s2, s3] = sides;
return switch ((s1, s2, s3)) {
(double x, double y, double z) when x + y <= z =>
'Not a valid triangle',
(double x, double y, double z) when x == y && y == z =>
'Equilateral',
(double x, double y, double z) when x == y || y == z =>
'Isosceles',
_ => 'Scalene',
};
}
void main() {
print(classifyAge(5)); // Child
print(classifyAge(15)); // Teenager
print(classifyAge(30)); // Adult
print(classifyAge(70)); // Senior
print(classifyTriangle(3, 3, 3)); // Equilateral
print(classifyTriangle(3, 3, 5)); // Isosceles
print(classifyTriangle(3, 4, 5)); // Scalene
print(classifyTriangle(1, 2, 10)); // Not a valid triangle
}
التبديل الشامل
عندما تتحول على فئة مختومة، أو تعداد، أو قيمة منطقية، يمكن لـ Dart التحقق في وقت التجميع أنك غطيت كل حالة ممكنة. يسمى هذا فحص الشمولية. إذا فاتتك حالة، يخبرك المُجمع.
Switch شامل مع فئات مختومة
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Square extends Shape {
final double side;
Square(this.side);
}
class Triangle extends Shape {
final double base;
final double height;
Triangle(this.base, this.height);
}
// Dart تضمن تغطية جميع الحالات -- لا حاجة لـ default!
double area(Shape shape) => switch (shape) {
Circle(radius: var r) => 3.14159 * r * r,
Square(side: var s) => s * s,
Triangle(base: var b, height: var h) => 0.5 * b * h,
};
// إذا أضفت فئة Shape فرعية جديدة بدون تحديث هذا الـ switch،
// سينتج المُجمع خطأ!
// Switch شامل مع التعدادات
enum TrafficLight { red, yellow, green }
String action(TrafficLight light) => switch (light) {
TrafficLight.red => 'Stop',
TrafficLight.yellow => 'Slow down',
TrafficLight.green => 'Go',
// لا حاجة لـ default -- المُجمع يعرف أن جميع الحالات مغطاة
};
void main() {
print(area(Circle(5))); // 78.53975
print(area(Square(4))); // 16.0
print(area(Triangle(6, 3))); // 9.0
print(action(TrafficLight.green)); // Go
}
sealed، والتعدادات، والقيم المنطقية. إذا تحولت على فئة عادية أو Object، لا تستطيع Dart معرفة جميع الأنواع الفرعية الممكنة، لذا يجب تضمين default أو حالة البدل _. دائماً فضّل sealed على abstract عندما يكون لديك مجموعة ثابتة من الأنواع الفرعية.الأنماط المنطقية (&& و ||)
الأنماط المنطقية تجمع عدة أنماط فرعية. نمط || يطابق إذا تطابق أي نمط فرعي. نمط && يطابق فقط إذا تطابق كلا النمطين الفرعيين.
أنماط OR و AND المنطقية
void main() {
// نمط OR: مطابقة أي من عدة قيم
for (final char in 'Hello World 123!'.split('')) {
final type = switch (char) {
'a' || 'e' || 'i' || 'o' || 'u' ||
'A' || 'E' || 'I' || 'O' || 'U' => 'vowel',
' ' => 'space',
String c when c.contains(RegExp(r'[0-9]')) => 'digit',
String c when c.contains(RegExp(r'[a-zA-Z]')) => 'consonant',
_ => 'other',
};
// ...
}
// نمط AND: دمج فحص النوع مع قيد القيمة
final value = 42;
switch (value) {
case int n && > 0:
print('Positive int: $n'); // Positive int: 42
}
}
أنماط فحص وتأكيد القيم الفارغة
هذه الأنماط توفر طرقاً موجزة لمعالجة القيم القابلة للإلغاء ضمن مطابقة الأنماط.
أنماط فحص وتأكيد القيم الفارغة
void main() {
// نمط فحص القيمة الفارغة: يطابق القيم غير الفارغة، يفكك
String? maybeName = 'Edrees';
switch (maybeName) {
case String name?:
// علامة ? تعني "طابق إذا غير فارغ"
print('Name: $name'); // Name: Edrees
case null:
print('No name');
}
// فحص القيمة الفارغة في if-case
final Map<String, int> scores = {'math': 95, 'science': 88};
final int? mathScore = scores['math'];
if (mathScore case int score?) {
print('Math score: $score'); // Math score: 95
}
// نمط تأكيد القيمة الفارغة: يؤكد أنها غير فارغة (يرمي إذا فارغة)
final (String name, int? age) = ('Edrees', 28);
switch ((name, age)) {
case (String n, int a!):
// a! تعني "هذا يجب أن يكون غير فارغ" -- يرمي إذا فارغ
print('$n is $a years old'); // Edrees is 28 years old
}
}
?) على نمط تأكيد القيمة الفارغة (!) في معظم الحالات. نمط الفحص يمرر بأمان إلى الحالة التالية إذا كانت القيمة فارغة، بينما نمط التأكيد يرمي استثناء وقت التشغيل. استخدم ! فقط عندما تكون متأكداً أن القيمة لا يمكن أن تكون فارغة وتريد التعبير عن هذا اليقين.مثال عملي: تحليل JSON
مطابقة الأنماط تتفوق في تحليل والتحقق من صحة البيانات المنظمة مثل JSON. إليك مثال واقعي لتحليل استجابات API.
تحليل JSON آمن الأنواع مع الأنماط
import 'dart:convert';
sealed class ApiResult {}
class Success extends ApiResult {
final List<Map<String, dynamic>> users;
Success(this.users);
}
class ApiError extends ApiResult {
final String message;
final int code;
ApiError(this.message, this.code);
}
ApiResult parseResponse(String jsonString) {
final json = jsonDecode(jsonString);
return switch (json) {
{'status': 'success', 'data': List users} =>
Success(users.cast<Map<String, dynamic>>()),
{'status': 'error', 'message': String msg, 'code': int code} =>
ApiError(msg, code),
{'status': 'error', 'message': String msg} =>
ApiError(msg, 0),
_ => ApiError('Unknown response format', -1),
};
}
void main() {
final successJson = '{"status":"success","data":[{"name":"Edrees","age":28}]}';
final errorJson = '{"status":"error","message":"Not found","code":404}';
final result1 = parseResponse(successJson);
final result2 = parseResponse(errorJson);
// Switch شامل -- المُجمع يضمن معالجة جميع الحالات
switch (result1) {
case Success(users: var users):
for (final user in users) {
if (user case {'name': String name, 'age': int age}) {
print('User: $name, Age: $age');
}
}
case ApiError(message: var msg, code: var code):
print('Error $code: $msg');
}
}
مثال عملي: آلة الحالات
مطابقة الأنماط مع الفئات المختومة تنشئ آلات حالات أنيقة وآمنة الأنواع.
آلة حالات مع فئات مختومة وأنماط
sealed class AuthState {}
class Unauthenticated extends AuthState {}
class Authenticating extends AuthState {
final String email;
Authenticating(this.email);
}
class Authenticated extends AuthState {
final String userId;
final String token;
Authenticated(this.userId, this.token);
}
class AuthError extends AuthState {
final String message;
final int attempts;
AuthError(this.message, this.attempts);
}
sealed class AuthEvent {}
class LoginRequested extends AuthEvent {
final String email;
final String password;
LoginRequested(this.email, this.password);
}
class LogoutRequested extends AuthEvent {}
class TokenExpired extends AuthEvent {}
AuthState handleEvent(AuthState state, AuthEvent event) =>
switch ((state, event)) {
// من غير مصادق: يمكن فقط تسجيل الدخول
(Unauthenticated(), LoginRequested(email: var e, :var password)) =>
Authenticating(e),
// من المصادقة: محاكاة نجاح/فشل
(Authenticating(email: var e), _) when e.contains('@') =>
Authenticated('user_123', 'token_abc'),
(Authenticating(), _) =>
AuthError('Invalid email format', 1),
// من مصادق: يمكن تسجيل الخروج أو معالجة انتهاء الرمز
(Authenticated(), LogoutRequested()) =>
Unauthenticated(),
(Authenticated(), TokenExpired()) =>
Unauthenticated(),
// من الخطأ: يمكن إعادة محاولة تسجيل الدخول
(AuthError(attempts: var a), LoginRequested(:var email, :var password))
when a < 3 =>
Authenticating(email),
(AuthError(attempts: var a), LoginRequested()) when a >= 3 =>
AuthError('Too many attempts. Account locked.', a),
// الافتراضي: تجاهل الأحداث غير المتوقعة
(var currentState, _) => currentState,
};
void main() {
AuthState state = Unauthenticated();
print('Initial: $state');
state = handleEvent(state, LoginRequested('edrees@example.com', 'pass123'));
print('After login request: ${state.runtimeType}');
state = handleEvent(state, LogoutRequested());
print('After processing: ${state.runtimeType}');
}
الملخص
مطابقة الأنماط في Dart 3 هي نظام شامل يُحوّل طريقة كتابتك للمنطق الشرطي. من مطابقة الثوابت البسيطة إلى تفكيك الكائنات المتداخلة المعقدة مع شروط الحراسة، تجعل الأنماط الكود أكثر تصريحية وأماناً وسهولة في الصيانة. اجمع الأنماط مع الفئات المختومة لفحص الأنواع الشامل، واستخدم تعبيرات switch لحساب القيم الموجز، واستفد من if-case لشروط النمط الواحد. أتقن هذه الأدوات وسيكون كود Dart الخاص بك أنظف وأكثر متانة بشكل كبير.