اذهب إلى المحتوى

محمد بغات

الأعضاء
  • المساهمات

    177
  • تاريخ الانضمام

  • تاريخ آخر زيارة

  • عدد الأيام التي تصدر بها

    6

كل منشورات العضو محمد بغات

  1. تتشابه المراجع في سلوكها وتختلف عن المؤشّرات الثابتة (const pointers)، وتُعرَّف عن طريق إتباع الرمز ‎&‎ ‏‏(ampersand) باسم نوع. int i = 10; int &refi = i; يمثّل ‎refi‎ في المثال أعلاه مرجعًا مربوطًا (reference bound) إلى ‎i‎، كذلك فإن المراجع تُجرِّد مفهوم المؤشرات وتتصرف كاسم للكائن المُشار إليه: refi = 20; // i = 20; يمكنك أيضًا تعريف عدة مراجع في تعريف واحد: int i = 10, j = 20; int &refi = i, &refj = j; // خطأ شائع // int& refi = i, k = j; في المثال السابق ستكون refi من نوع ‎&int رغم أن k ستكون من نوع int وليس ‎&int. أيضًا، يجب تهيئة المراجع بشكل صحيح في التعريف، ولن يمكنك تعديلها بعد ذلك. انظر المثال التالي حيث تؤدي الشيفرة إلى خطأ في التصريف لأن الإعلان عن متغير المرجع i يتطلب مهيِّئًا. int &i; كذلك لا يمكن ربط مرجع إلى المؤشر الفارغ ‎nullptr‎ مباشرة، على عكس المؤشرات، انظر المثال التالي حيث نحصل على خطأ لاستحالة ربط مرجع غير ثابت لقيمة يسارية من نوع int بعنصر من نوع nullptr_t: int *const ptri = nullptr; int &refi = nullptr; الدلالات القيميّة والمرجعية يكون لنوع ما دلالة قيميّة (value semantics) إذا كانت حالة الكائن القابلة للمُلاحظة مختلفة وظيفيًا عن جميع الكائنات الأخرى من ذلك النوع. هذا يعني أنه إذا نسخت أحد الكائنات فستحصل على كائن جديد، ولن تؤثر التعديلات على الكائن الجديد بأي شكل من الأشكال على الكائن القديم. معظم أنواع C++‎ الأساسية لها دلالات قيميّة، انظر المثال التالي حيث تطبع std::cout << i الرقم 5 لأن i لم تتأثر بالتغييرات التي أجريت على j. int i = 5; int j = i; // منسوخ j += 20; std::cout << i; // j لم تتأثر بالتغييرات التي أجريناها على i تطبع 5، لأنّ معظم الأنواع المُعرّفة في المكتبة القياسية لها دلالة قيميّة أيضًا: std::vector<int> v1(5, 12); // مصفوفة خماسية كل قيمها تساوي 12 std::vector<int> v2 = v1; // نسخ المتجهة v2[3] = 6; v2[4] = 9; std::cout << v1[3] << " " << v1[4]; // "12 12" يقال إنّ نوعًا ما له دلالة مرجعيّة (reference semantics) إذا تشاركت نُسخ ذلك النوع حالتها القابلة للملاحظة مع كائنات أخرى (خارجية)، بحيث يؤدي تعديل كائن واحد إلى تغيير حالة كائن آخر، وللمؤشّرات دلالة قيميّة في C++‎ فيما يتعلّق بهويّة الكائن الذي تشير إليه، ولها دلالة مرجعيّة فيما يتعلق بحالة الكائن الذي تشير إليه: int *pi = new int(4); int *pi2 = pi; pi = new int(16); assert(pi2 != pi); // تتحقق دائما int *pj = pi; *pj += 5; std::cout << *pi; // يشيران إلى نفس العنصر pj و pi تطبع 9 لأن كذلك فإن مراجع C++‎ لها دلالة مرجعية. النسخ العميق ودعم النقل إذا كنت تريد أن تجعل لنوع ما دلالة قيميّة وكان ذلك النوع يحتاج إلى تخزين الكائنات الديناميكية (dynamically allocated)، فسيحتاج ذلك النوع عند عمليات النسخ إلى تخصيص (allocate) نسخ جديدة من تلك الكائنات، كما يجب أن يفعل ذلك أيضًا عند تعيين النسخة (copy assignment). يُسمّى هذا النوع من النسخ "النسخ العميق" (deep copy) إذ أنّه يحوّل الدلالة المرجعية إلى دلالة قيمية: struct Inner { int i; }; const int NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {}~Value() { delete[] array_; } Value(const Value &val): array_(new Inner[NUM_INNER]) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; } Value &operator = (const Value &val) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; return *this; } }; الإصدار ≥ C++‎ 11 تسمح الدلالة النقليّة (Move semantics) لنوع مثل الصنف ‎Value‎ أن يتجنب نسخ بياناته المرجعية (referenced data)، وإذا استعمل المُستخدم القيمة بطريقة تؤدي إلى نقلٍ (move) فيمكن أن يؤدّي ذلك إلى تفريغ الكائن المنسوخ من البيانات التي أشار إليها: struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {} // nullptr يُسمح بالنقل حتى لو أعادت ~Value() { delete[] array_; } Value(const Value &val): array_(new Inner[NUM_INNER]) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; } Value &operator = (const Value &val) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; return *this; } // النقل يعني لا تخصيص للذاكرة // لا يمكن إطلاق اعتراض Value(Value &&val) noexcept: array_(val.array_) { // لقد أخذنا القيمة القديمة val.array_ = nullptr; } // لا يمكن رفع اعتراضات Value &operator = (Value &&val) noexcept { // ستُدمّر قريبا val حيلة ذكية، لأن // لقد استبدلنا بياناتنا ببياناته، وسيدمّر مدمّره بياناتنا std::swap(array_, val.array_); } }; لا شك أننا نستطيع جعل مثل هذا النوع غير قابل للنسخ إن أردنا منع النسخ العميق مع السماح بنقل الكائن، انظر: struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {} // nullptr يُسمح بالنقل حتى لو أعادت ~Value() { delete[] array_; } Value(const Value &val) = delete; Value &operator = (const Value &val) = delete; // النقل يعني لا تخصيص للذاكرة // لا يمكن إطلاق اعتراض Value(Value &&val) noexcept: array_(val.array_) { // لقد أخذنا القيمة القديمة val.array_ = nullptr; } // لا يمكن رفع اعتراضات Value &operator = (Value &&val) noexcept { // ستُدمّر قريبا val حيلة ذكية، لأن // لقد استبدلنا بياناتنا ببياناته، مدمّره سيدمّر بياناتنا std::swap(array_, val.array_); } }; نستطيع أن نطبق قاعدة الصفر (Rule of Zero) من خلال استخدام مؤشّر فريد ‎unique_ptr‎: struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: unique_ptr<Inner []>array_; // نوع للنقل فقط public: Value(): array_(new Inner[NUM_INNER]) {} // لا داعي للحذف الصريح، أو حتى التصريح ~Value() = default; { delete[] array_; } // لا داعي للحذف الصريح، أو حتى التصريح Value(const Value &val) = default; Value &operator = (const Value &val) = default; // تنفيذ النقل عنصرا بعنصر Value(Value &&val) noexcept = default; // تنفيذ النقل عنصرا بعنصر Value &operator = (Value &&val) noexcept = default; }; هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 26: References والفصل Chapter 27: Value and Reference Semantics من كتاب C++ Notes for Professionals
  2. تعريف الأصناف متعددة الأشكال أبسط مثال لتوضيح مفهوم تعددية الأشكال (Polymorphism) أنه إنشاء لصنف مجرّد يصف الأشكال الهندسية، والذي يمكن أن نشتق منه المربّعات والدوائر وغيرها من الأشكال الأخرى. الصنف الأب لنبدأ بالصنف متعدد الأشكال (polymorphic class): class Shape { public: virtual~Shape() = default; virtual double get_surface() const = 0; virtual void describe_object() const { std::cout << "this is a shape" << std::endl; } double get_doubled_surface() const { return 2 * get_surface(); } }; تحليل الشيفرة: تستطيع تعريف السلوك متعدد الأشكال عن طريق إسباق توابع الصنف باستخدام الكلمة المفتاحية ‎virtual‎، فهنا سيُقَدَّم التابع ‎get_surface()‎ الخاص بالمربع بشكل مختلف عن ‎describe_object()‎ الخاص بالدائرة، عند استدعاء الدالة على كائن ما، ستُستدعى الدالة المقابلة للصنف الحقيقي للكائن في وقت التشغيل. لا معنى لتعريف التابع ‎get_surface()‎ لشكلٍ مجرّد، لهذا تُتبَع الدالة بالتعبير ‎= 0‎، في إشارة إلى أنّ الدالة إنّما هي دالة وهمية خالصة. يجب أن تعرِّف الأصناف متعددة الأشكال مُدمِّرًا وهميًا (virtual destructor) دومًا. يجوز لك تعريف دوال تابعة غير وهمية، وسيُختار التابع المناسب عند استدعاء تلك التوابع على كائن ما وفقًا للصنف المستخدم في وقت التصريف، وقد عرَّفنا التابع ‎get_double_surface()‎ في المثال أعلاه بهذه الطريقة. الصنف الذي يحتوي على دالة وهميّة واحدة على الأقل يُعدُّ صنفًا مجرّدًا، ولا يمكن للأصناف المجرّدة أن تُستنسخ (instantiated)، وإنما تستطيع أن تحصل فقط على مؤشرات أو مراجع إليها. الأصناف المشتقة (Derived classes) بمجرد تعريف صنف أساسي متعدد الأشكال فيمكنك الاشتقاق منه، انظر: class Square: public Shape { Point top_left; double side_length; public: Square(const Point& top_left, double side): top_left(top_left), side_length(side_length) {} double get_surface() override { return side_length * side_length; } void describe_object() override { std::cout << "this is a square starting at " << top_left.x << ", " << top_left.y << " with a length of " << side_length << std::endl; } }; تفسير المثال: يمكنك تعريف أو إعادة تعريف (override) أيٍّ من الدوال الوهميّة للصنف الأب، وستبقى الدوال الوهميّة في الصنف الأب وهميّة أيضًا في الصنف المشتق، ولا حاجة لإضافة الكلمة المفتاحية ‎virtual‎ مرّة أخرى، لكن يُوصى بإضافة الكلمة المفتاحية ‎override‎ إلى نهاية تصريح الدالة لمنع الأخطاء الناتجة عن الاختلافات غير الملحوظة في بصمة الدالّة. إذا عُرِّفت جميع الدوالّ الوهميّة للصنف الأب، فسيكون بمقدورك استنساخ كائنات من ذلك الصنف، أما إن بقيت دوالّ وهمية غير مُعرّفة فإنّ الصنف المشتق سيبقى صنفًا مجردًا. لست ملزمًا بإعادة تعريف كل الدوالّ الوهميّة، بل لا بأس بالاحتفاظ بنسخة الصنف الأب إن كان ذلك يناسب ما تحتاجه. انظر المثال التالي على الاستنساخ: int main() { Square square(Point(10.0, 0.0), 6); // نعلم أنه مربع square.describe_object(); std::cout << "Surface: " << square.get_surface() << std::endl; Circle circle(Point(0.0, 0.0), 5); Shape *ps = nullptr; // لا نعلم بعدُ النوع الحقيقي للكائن ps = &circle; // إنها دائرة، لكن كان من الممكن أن تكون مربعا ps->describe_object(); std::cout << "Surface: " << ps - > get_surface() << std::endl; } التخفيض الآمن (Safe downcasting) لنفترض أنّ لديك مؤشرًا يشير إلى كائن من صنف متعدد الأشكال: Shape *ps; ps = get_a_new_random_shape(); // إذا لم تكن لديك هذه الدالة // ps = new Square(0.0,0.0, 5); :يمكنك أن تكتب فيكون التخفيض هنا هو أن تحوّل الصنف العام متعدد الأشكال ‎Shape‎ إلى أحد الأشكال المشتقة منه، مثل ‎Square‎ أو ‎Circle‎. لماذا التخفيض؟ لن تحتاج في معظم الأحيان إلى معرفة النوع الحقيقي للكائن لأنّ الدوال الوهميّة تسمح لك بمعالجة الكائن بشكل مستقل عن نوعه: std::cout << "Surface: " << ps->get_surface() << std::endl; أما إذا لم تحتج إلى التخفيض فهذا مؤشّر على أنّ شيفرتك جيدة، لكن قد تحتاج إليه في بعض الأحيان حين تريد استدعاء دالة غير وهميّة لا توجد إلا في الصنف الفرعي. على سبيل المثال، يختص مفهوم القطر بالدوائر فقط، لذلك سيُعرَّف الصنف على النحو التالي: class Circle: public Shape { Point center; double radius; public: Circle(const Point& center, double radius): center(center), radius(radius) {} double get_surface() const override { return r * r * M_PI; } // هذا صالح للدوائر وحسب، ولا معنى له بالنسبة لبقية الأشكال double get_diameter() const { return 2 * r; } }; لا توجد دالة التابع ‎get_diameter()‎ إلا في الدوائر، ولم تُعرَّف لكائن (‎Shape‎) عام: Shape* ps = get_any_shape(); ps->get_diameter(); // خطأ في التصريف كيفية التخفيض إذا كنت متأكدًا أنّ المؤشّر ‎ps‎ يشير إلى دائرة، فيمكنك استخدام ‎static_cast‎: std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl; هذا سيؤدي الغرض لكنه ينطوي على مخاطرة كبيرة، فإذا لم يكن المؤشّر ‎ps‎ يشير إلى دائرة (‎Circle‎)، فلا يمكن توقّع سلوك الشيفرة. لذلك يُفضّل استخدام الخيار الآمن ‎dynamic_cast‎ الذي يختص بالأصناف متعددة الأشكال: int main() { Circle circle(Point(0.0, 0.0), 10); Shape &shape = circle; std::cout << "The shape has a surface of " << shape.get_surface() << std::endl; //shape.get_diameter(); // خطأ في التصريف Circle *pc = dynamic_cast < Circle * > ( & shape); // nullptr يشير إلى دائرة فسنحصل على ps إذا لم يكن المؤشر if (pc) std::cout << "The shape is a circle of diameter " << pc - > get_diameter() << std::endl; else std::cout << "The shape isn't a circle !" << std::endl; } لاحظ أنّ ‎dynamic_cast‎ غير ممكنة على الأصناف ذات الشكل الواحد، وستحتاج إلى دالة وهميّة واحدة على الأقل في الصنف أو أحد آبائه لتتمكن من استخدامها. التعددية الشكلية والمدمِّرَات (Polymorphism & Destructors) إذ أردت استخدام الصنف بحيث يكون متعدد الأشكال وتُخزَّن النسخ (instances) المشتقّة منه كمؤشّرات/مراجع أساسية، فيجب أن يكون مدمِّر الصنف الأساسي وهميًّا (‎virtual‎) أو محميًا (‎protected‎)، وفي الحالة الأولى سيؤدي تدمير الكائن إلى التحقّق من ‎vtable‎ وسَيستدعي المدمّر الصحيح تلقائيًا بناءً على النوع الديناميكي. أما في الثانية فتُعطَّل إمكانية تدمير الكائن عبر مؤشّر/مرجع للصنف الأساسي، ولا يمكن حذف الكائن إلا عند التعامل معه باعتباره من نوعه الفعلي. struct VirtualDestructor { virtual~VirtualDestructor() = default; }; struct VirtualDerived: VirtualDestructor {}; struct ProtectedDestructor { protected: ~ProtectedDestructor() = default; }; struct ProtectedDerived: ProtectedDestructor { ~ProtectedDerived() = default; }; // ... VirtualDestructor* vd = new VirtualDerived; delete vd; ProtectedDestructor* pd = new ProtectedDerived; delete pd; // Error: ProtectedDestructor::~ProtectedDestructor() is protected. delete static_cast < ProtectedDerived* > (pd); // OK سيبحث delete vd عن ()VirtualDestructor::~VirtualDestructor في vtable وعند وجوده سيبحث عن ()VirtualDerived::~VirtualDerived ويستدعيه. كذلك سيعطي delete pd خطأً لأن ()ProtectedDestructor::~ProtectedDestructor محميّ. سيضمن هذا أنّ مدمّر الصنف المشتق سيُستدعى على جميع نُسخ الصّنف المشتق، ممّا يمنع تسرّب الذاكرة. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 25: Polymorphism من كتاب C++ Notes for Professionals
  3. استدعاء الدوال بالقيمة أو بالمرجع هدف هذا القسم هو شرح الاختلافات من الناحية النظرية والعمليّة بخصوص ما يحدث لمعاملات الدوال عند استدعائها، وأيضًا قابلية إعادة استخدام المتغيرات وقيمها بعد استدعاء الدالة، إذ يمكن النظر إلى المعامِلات على أنّها متغيّرات قبل استدعاء الدالّة وداخل الدالّة، إذ يختلف سلوك هذه المتغيرات وقابلية الوصول لها بحسب الطريقة المستخدمة لتمريرها. الاستدعاء بالقيمة (Call by value) عند استدعاء دالة، تُنشأ عناصر جديدة في مُكدِّس (stack) البرنامج، ويشمل ذلك بعض المعلومات عن الدالّة وكذلك المساحة (مواقع الذاكرة) المخصّصة للمعامِلات، والقيمة المُعادة. وعند تمرير معامِل إلى دالّة، تُنسخ قيمة المتغير المُستخدم (أو العنصر الحرفي) إلى الموقع المخصّص في الذاكرة لمُعامل الدالة، هذا يعني أنه سيُوجد حينها موقعان في الذاكرة لهما نفس القيمة، وسنعمل دَاخل الدالة على الموقع المخصّص للمعامل في الذاكرة فقط. تُحذَف الذاكرة الموجودة في مُكدّس البرنامج بعد الخروج من الدالّة ممّا يؤدّي إلى مسح جميع بيانات استدعاء الدالّة بما في ذلك المواقع في الذاكرة المخصّصة للمعاملات التي استخدمناها داخلَ الدالّة، ومن ثم فإنّ القيم التي غُيِّرت داخل الدالّة لن تؤثر على قيم المتغيرات الخارجية. int func(int f, int b) { تُنشأ متغيرات جديدة، وتحمل القيم المنسوخة من خارج f القيمة 0، وقيمة inner_b تساوي 1. نتابع: f = 1; // تساوي 1 f قيمة b = 2; // تساوي 2 inner_b قيمة return f + b; } int main(void) { int a = 0; int b = 1; //outer_b int c; c = func(a, b); // c تُنسخ القيمة المعادة إلى // تساوي الصفر a // تساوي 1 outer_b قيمة // هما متغيّران مختلفان outer_b و inner_b // تساوي 3 c قيمة } في المثال السابق نشئ متغيرات داخل الدالّة الرئيسية، وعند استدعاء الدوالّ يُنشأ متغيران جديدان: ‎f‎ و ‎inner_b‎، حيث يتشارك ‎b‎ الاسم مع المتغيّر الخارجي ولكن لا يتشارك معه في موقع الذاكرة، كما أنّ سلوك ‎a<->f‎ و ‎b<->b‎ متماثلان هنا. يوضح الرسم التالي ما يحدث في المُكدّس ولماذا لا يحدث أيّ تغيير على المتغيّر ‎b‎. هذا الرسم ليس دقيقًا تمامًا، ولكنّه يوضّح المثال. يُطلق على هذا "استدعاءً بالقيمة" لأننا لا نمرّر المتغيرات نفسها، وإنما نمرّر قيمها فقط. وإعادة عدة قيم من دالة قد تحتاج أحيانًا أن تعيد عدة قيم من دالة ما، كأن ترغب في إدخال عنصر وتعيد ثمنه ورقمه في المخزن مثلًا، فهذه الوظيفة تكون مفيدة عندها، ولدينا عدة طرق لتنفيذ ذلك في ++C باستخدام مكتبة القوالب القياسية، وتستطيع تجنب هذه المكتبة إن احتجت ذلك، وسيزال لديك عدة طرق أخرى بما فيها البُنى والأصناف والمصفوفات. استخدام الصّفوف std::tuple الإصدار ≥ C++‎ 11 تستطيع الصفوف (‎std::tuple‎) أن تجمّع أيّ عدد من القيم حتى لو كانت من أنواع مختلفة: std::tuple < int, int, int, int > foo(int a, int b) { // or auto (C++14) return std::make_tuple(a + b, a - b, a * b, a / b); } في C++‎ 17، يمكن استخدام قائمة مهيِّئة ذات أقواس معقوصة (braced initializer list): std::tuple<int, int, int, int> foo(int a, int b) { return {a + b, a - b, a * b, a / b}; } قد تكون استعادة القيم المعادة من ‎tuple‎ مرهقة إذ تتطلّب استخدام دالة القالب ‎std::get‎: auto mrvs = foo(5, 12); auto add = std::get<0>(mrvs); auto sub = std::get<1>(mrvs); auto mul = std::get<2>(mrvs); auto div = std::get<3>(mrvs); إذا أمكن التصريح عن الأنواع قبل عودة الدالّة، فيمكن استخدام ‎std::tie‎ لتفريغ الصف ‎tuple‎ في متغيّرات أخرى: int add, sub, mul, div; std::tie(add, sub, mul, div) = foo(5, 12); يمكن استخدام ‎std::ignore‎ إذا انتفت الحاجة إلى قيمة من القيم المعادة: std::tie(add, sub, std::ignore, div) = foo(5, 12); الإصدار ≥ C++‎ 17 يمكن استخدام الارتباطات البنيوية (Structured bindings) لتجنّب استخدام ‎std::tie‎: auto [add, sub, mul, div] = foo(5,12); إذا أردت إعادة صفٍّ مؤلَّف من مراجعِ القيم اليسارية (lvalue references) بدلًا من صفّ مؤلّف من القيم، فاستخدم ‎std::tie‎ بدلاً من std::make_tuple. std::tuple < int & , int & > minmax(int & a, int & b) { if (b < a) return std::tie(b, a); else return std::tie(a, b); } والذي يسمح بما يلي: void increase_least(int& a, int& b) { std::get < 0 > (minmax(a, b)) ++; } في بعض الحالات النادرة، قد تُستخدم ‎std::forward_as_tuple‎ بدلاً من ‎std::tie‎ لكن احذر في تلك الحالات إذ قد لا تدوم الكائنات المؤقّتة بما يكفي لاستهلاكها. الارتباطات البنيوية (Structured Bindings) الإصدار ≥ C++‎ 17 قدّم الإصدار C++‎ 17 مفهوم الارتباطات البنيويّة التي سهّلت على المبرمجين التعامل مع عدة أنواع للإعادة، إذ أنّك لن تكون مضطرًّا إلى الاعتماد على ‎std::tie()‎ أو تفريغ الصفوف يدويًِّا: std::map < std::string, int > m; // أدرج عنصرا في القاموس وتحقق من نجاح الإدراج auto[iterator, success] = m.insert({ "Hello", 42 }); if (success) { // شيفرتك هنا } // 'second' و 'first' كرّر على كل العناصر دون الحاجة إلى استخدام الاسمين المبهميْن for (auto const& [key, value]: m) { std::cout << "The value for " << key << " is " << value << '\n'; } يمكن استخدام الارتباطات البنيويّة افتراضيًّا مع الأزواج (‎std::pair‎) والصفوف (‎std::tuple‎) وكذلك أيّ نوع تكون بياناته العضويّة غير الساكنة (non-static data members) إما أعضاءً مباشرين علنيين أو أعضاءً من صنف أساسي محدد: struct A { int x; }; struct B: A { int y; }; B foo(); // استخدام الارتباطات البنيويّة const auto[x, y] = foo(); // الشيفرة المكافئة بدون استخدام الارتباطات البنيويّة const auto result = foo(); auto& x = result.x; auto& y = result.y; إذا جعلت نوعًا ما "شبيهًا بالصفوف (tuple-like)" فسيعمل تلقائيًا مع ذلك النوع. النوع الشبيه بالصّفوف يتوفّر على التوابع التالية: ‎tuple_size‎ و ‎tuple_element‎ و ‎get‎: namespace my_ns { struct my_type { int x; double d; std::string s; }; struct my_type_view { my_type* ptr; }; } namespace std { template<> struct tuple_size<my_ns::my_type_view> : std::integral_constant<std::size_t, 3> {}; template<> struct tuple_element<my_ns::my_type_view, 0>{ using type = int; }; template<> struct tuple_element<my_ns::my_type_view, 1>{ using type = double; }; template<> struct tuple_element<my_ns::my_type_view, 2>{ using type = std::string; }; } namespace my_ns { template<std::size_t I> decltype(auto) get(my_type_view const& v) { if constexpr (I == 0) return v.ptr->x; else if constexpr (I == 1) return v.ptr->d; else if constexpr (I == 2) return v.ptr->s; static_assert(I < 3, "Only 3 elements"); } } ستعمل الآن الشيفرة التالية: my_ns::my_type t{1, 3.14, "hello world"}; my_ns::my_type_view foo() { return {&t}; } int main() { auto[x, d, s] = foo(); std::cout << x << ',' << d << ',' << s << '\n'; } استخدام البُنى struct يمكن استخدام البُنى (‎struct‎) لتجميع عدّة قيم لكي تعيدها دالة: الإصدار C++‎ 11 struct foo_return_type { int add; int sub; int mul; int div; }; foo_return_type foo(int a, int b) { return {a + b, a - b, a * b, a / b}; } auto calc = foo(5, 12); الإصدار ++‎> يمكن استخدام مُنشئ لتبسيط عمليّة إنشاء القيم المُعادة، بدلاً من الإسناد إلى حقول الصنف فرادى: struct foo_return_type { int add; int sub; int mul; int div; foo_return_type(int add, int sub, int mul, int div): add(add), sub(sub), mul(mul), div(div) {} }; foo_return_type foo(int a, int b) { return foo_return_type(a + b, a - b, a * b, a / b); } foo_return_type calc = foo(5, 12); يمكن استرداد النتائج الفردية المُعادة من قِبل دالة ‎foo()‎ عن طريق الوصول إلى المتغيرات الأعضاء لبُنية calc: std::cout << calc.add << ' ' << calc.sub << ' ' << calc.mul << ' ' << calc.div << '\n'; الناتج: 17 -7 60 0 ملاحظة: ، تُجمَّع القيم المُعادة معًا عند استخدام البُنى في كائن واحد، ويمكن الوصول إليها باستخدام أسماء ذات معنى (meaningful)، وهذا يساعد على تقليل عدد المتغيرات الخارجية التي أنشئت في نطاق القيم المُعادة. الإصدار C++‎ 17 ويمكن استخدام الارتباطات البنيويّة لتفريغ بُنية struct المُعادة من دالة، وهذا يجعل معامِلات الخرج على قدم واحدة مع معامِلات الدخل، انظر: int a=5, b=12; auto[add, sub, mul, div] = foo(a, b); std::cout << add << ' ' << sub << ' ' << mul << ' ' << div << '\n'; يطابق خرْجُ هذه الشيفرة خرجَ الشيفرة أعلاه، فما زالت struct تُستخدم لإعادة القيم من الدالة مما يسمح لك بالتعامل مع الحقول بشكل فردي. استخدام معامِلات الخرج يمكن استخدام المعاملات لإعادة قيمة واحدة أو أكثر بشرط أن تكون تلك المعاملاتُ مؤشّراتٍ أو مراجعَ غير ثابتة. المراجع: void calculate(int a, int b, int & c, int & d, int & e, int & f) { c = a + b; d = a - b; e = a * b; f = a / b; } المؤشرات: void calculate(int a, int b, int * c, int * d, int * e, int * f) { *c = a + b; *d = a - b; *e = a * b; *f = a / b; } تَستخدم بعض المكتبات والأُطُر التعليمة البرمجية ‎#define OUT لتحديد المعاملاتِ التي ستكون معاملاتِ الخرج في بصمة الدالة، رغم انعدام التأثير الوظيفي لها وأنها ستُهمَل عند التصريف، لكنها ستجعل بصمة الدالة أكثر وضوحًا، انظر: #define OUT void calculate(int a, int b, OUT int& c) { c = a + b; } استخدام مستهلِك دالة (Function Object Consumer) نستطيع توفير مستهلك يُستدعى مع القيم المتعددة ذات الصّلة: الإصدار ++ C++‎ 11 template < class F > void foo(int a, int b, F consumer) { consumer(a + b, a - b, a * b, a / b); } // الاستخدام سهل، كما أنه يمكن تجاهل بعض النتائج foo(5, 12, [](int sum, int, int, int) { std::cout << "sum is " << sum << '\n'; }); يُعرَف هذا باسم "نمط التمرير المستمر" (continuation passing style)، يمكنك تكييف دالة تُعيد صفًّا إلى دالة ذات نمط تمرير مستمر عبر ما يلي: الإصدار ≥ C++‎ 17 template<class Tuple> struct continuation { Tuple t; template<class F> decltype(auto) operator->*(F&& f)&&{ return std::apply( std::forward<F>(f), std::move(t) ); } }; std::tuple<int,int,int,int> foo(int a, int b); continuation(foo(5,12))->*[](int sum, auto&&...) { std::cout << "sum is " << sum << '\n'; }; ستجد صيغًا أكثر تعقيدًا في C++‎ 14 أو C++‎ 11. استخدام قالب std::pair يستطيع قالب البُنية std::pair أن يجمع قيمتيْ إعادة معًا حتى لو كانا من نوعين مختلفين: #include <utility> std::pair < int, int > foo(int a, int b) { return std::make_pair(a + b, a - b); } في C++‎ 11 والإصدارات الأحدث، يمكن استخدام قائمة مُهيِّئة بدلاً من ‎std::make_pair‎: الإصدار C++‎ 11 #include <utility> std::pair<int, int> foo(int a, int b) { return {a+b, a-b}; } يمكن جلب القيم الفردية للزوج المعاد باستخدام العضوين ‎first‎ و ‎second‎: std::pair<int, int> mrvs = foo(5, 12); std::cout << mrvs.first + mrvs.second << std::endl; سيكون الناتج: 10 استخدام المصفوفات std::array الإصدار ≥ C++‎ 11 يُمكن لِمَصفوفة ما (‎std::array‎) أن تُجمّع معًا عددًا ثابتًا من القيم لغرض إعادتها، ويجب أن يكون عدد العناصر معروفًا عند التصريف، كذلك يجب أن تكون جميع القيم المعادة من نفس النوع: std::array<int, 4> bar(int a, int b) { return { a + b, a - b, a * b, a / b }; } تستبدل هذه الطريقةُ نمطَ صياغة المصفوفات في لغة ‏C‏‏ (‎int bar[4]‎)، فميزتها أنّه يمكن الآن استخدام دوال عديدة من مكتبة القوالب القياسية std الخاصة بلغة ‎c++‎، كما أنها توفر عدّة دوال تابعة مفيدة مثل ‎at‎ وهو تابع وصول آمن يتحقَّق من الحدود، وتابع ‎size‎ الذي يعيد حجم المصفوفة. استخدام مُكرّرات الخرج يمكن إعادة عدة قيم من نفس النوع بتمرير مُكرّر خرْج إلى الدالة، ويشيع هذا في الدوال العامة مثل خوارزميات المكتبة القياسية. انظر المثال التالي: template < typename Incrementable, typename OutputIterator > void generate_sequence(Incrementable from, Incrementable to, OutputIterator output) { for (Incrementable k = from; k != to; ++k) *output++ = k; } مثال تطبيقي: std::vector<int> digits; generate_sequence(0, 10, std::back_inserter(digits)); // {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} استخدام المتجهات ‎std::vector‎ تساعد المتجهات (‎std::vector‎) في إعادة عدد غير ثابت من المتغيّرات من نفس النوع. انظر المثال التالي حيث نستخدم ‎int‎ كنوع بيانات، رغم أن المتجهات يمكنها احتواء أي نوع قابل للنسخ، ستعيد الدالة كل الأعداد الصحيحة بين a و b في متجه ما، وسيكون الحد الأقصى من العناصر التي يمكن للدالة أن تعيدها هو std::vector::max_size على فرض أن ذاكرة النظام تستطيع احتواء ذلك الحجم. #include <vector> #include <iostream> std::vector < int > fillVectorFrom(int a, int b) { std::vector < int > temp; for (int i = a; i <= b; i++) { temp.push_back(i); } return temp; } ستعيِّن الشيفرة التالية المتجه الذي أنشئ داخل الدالة والمملوء إلى المتجه v الجديد، انظر: int main() { std::vector < int > v = fillVectorFrom(1, 10); // "1 2 3 4 5 6 7 8 9 10 " for (int i = 0; i < v.size(); i++) { std::cout << v[i] << " "; } std::cout << std::endl; return 0; } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 24: Returning several values from والفصل Chapter 28: C++ function "call by value" vs. "call by reference"‎ من كتاب C++ Notes for Professionals
  4. الكلمات المفتاحية هي كلمات لها معنى محدّد في C++‎ ولا يمكن استخدامها كمُعرّفات، كذلك لا يجوز إعادة تعريف الكلمات المفتاحية باستخدام المعالج المُسبق (preprocessor) في أيّ وحدة ترجمة تتضمن ترويسة المكتبة القياسية. لكن بأي حال فإن الكلمات المفتاحية تفقد معناها المميِّز داخل السمات (attributes). decltype الإصدار ≥ C++‎ 11 تعيد الكلمة المفتاحية decltype نوع المعامَل الخاص بها، والذي لا يُقيَّم. إذا كان المعامَل ‎e‎ اسمًا بدون أي أقواس إضافية، فإنّ ‎decltype(e)‎ ستكون النوع المُصرَّح للمعامل ‎e‎. انظر المثال التالي حيث تكون v من نوع <vector<int: int x = 42; std::vector<decltype(x)> v(100, x); إذا كان المعامَل ‎e‎ عضوًا من صنفٍ، ولم يكن محاطًا بأيّة أقواس، فإنّ ‎decltype(e)‎ ستكون النوع المُصرّح للعضو الذي تم الوصول إليه. انظر المثال التالي حيث تكون y من نوع int رغم أن s.x ثابتة. struct S { int x = 42; }; const S s; decltype(s.x) y; تعيد ‎decltype(e)‎ في جميع الحالات الأخرى كلًّا من نوع وصنف التعبير ‎e‎ كما يلي: إذا كانت ‎e‎ تعبيرًا يساريًا (lvalue) من النوع ‎T‎، فإنّ ‎decltype(e)‎ ستساوي ‎T&‎. إذا كانت ‎e‎ تعبيرًا من فئة (xvalue ) من ‎T‎، فإنّ ‎decltype(e)‎ ستساوي ‎T&&‎. إذا كانت ‎e‎ تعبيرًا من فئة (prvalue) من ‎T‎، فإنّ ‎decltype(e)‎ ستساوي ‎T‎. وهذا يشمل حالة الأقواس الخارجية: int f() { return 42; } int & g() { static int x = 42; return x; } int x = 42; decltype(f()) a = f(); decltype(g()) b = g(); decltype((x)) c = x; لاحظ في المثال السابق أن a من نوع int، و b من نوع &int، و c من نوع &int كذلك لأن x قيمة يسارية (lvalue). الإصدار ≥ C++‎ 14 يستنتج الشكل الخاص ‎decltype(auto)‎ نوعَ متغيّر ما من مُهيِّئه (initializer) أو نوع الإعادة لدالة من تعليمات ‎return‎ الموجودة في تعريف الدالة، باستخدام قواعد الاستنتاج الخاصة بـ ‎decltype‎ بدلاً من قواعد ‎auto‎. انظر المثال التالي حيث تكون y من نوع int، و z من نوع const int، وهو النوع المصرح من x. const int x = 123; auto y = x; decltype(auto) z = x; const تمثل const مُحدِّدًا للنوع، وتنتج النسخة المؤهلة ثبوتيًا من نوع ما عند تطبيقها على ذلك النوع. const int x = 123; x = 456; // خطأ int& r = x; // خطأ struct S { void f(); void g() const; }; const S s; s.f(); // خطأ s.g(); // OK تجنّب تكرار الشيفرة البرمجية في التوابع الجالبة (Getter Methods) يمكن زيادة تحميل التوابع التي لا تختلف إلا في مؤهِّل‏‏ ‎const‎، وقد تحتاج أحيانًا إلى نسختين من الجالب الذي يعيد مرجعًا إلى عضو ما. ولنفرض أن ‎Foo‎ صنف له تابعان يُجرِيان عمليّات متطابقة ويُعيدان مرجعًا إلى كائن من النوع ‎Bar‎، فإنه يكون على النحو التالي: class Foo { public: Bar & GetBar( /* some arguments */ ) { /* افعل شيئا ما هنا */ return bar; } const Bar & GetBar( /* some arguments */ ) const { /* افعل شيئا ما هنا */ return bar; } // ... }; الفرق الوحيد بينهما هو أنّ أحد التابعين غير ثابت (non-const) ويُعيد مرجعًا غير ثابت كذلك يمكن استخدامه لتعديل الكائن، أما الثاني فهو ثابت ويعيد مرجعًا ثابتًا. وقد نميل إلى استدعاء أحد التابعيْن من التابع الآخر من أجل تجنّب تكرار الشيفرة، لكن لا يمكننا استدعاء تابع غير ثابت من تابع ثابت، وإنما نستطيع استدعاء تابع ثابت من آخر غير ثابت، وسيتطلّب ذلك استخدام "const_cast" لإزالة مُؤهِّل const. انظر الحل فيما يلي: struct Foo { Bar & GetBar( /*arguments*/ ) { return const_cast < Bar & > (const_cast < const Foo * > (this) - > GetBar( /*arguments*/ )); } const Bar & GetBar( /*arguments*/ ) const { /* افعل شيئًا ما هنا */ return foo; } }; في الشيفرة أعلاه، استدعينا نسخة ثابتة من ‎GetBar‎ من التابع المتغير ‎GetBar‎ عن طريق تحويله إلى النوع الثابت const_cast<const Foo*>(this)‎. وبما أننا استدعينا تابعًا ثابتًا من آخرَ متغيرٍ فإن الكائن نفسه يكون متغيرًا، ويُسمح بإهمال الثابت. انظر المثال التالي لمزيد من الشرح: #include <iostream> class Student { public: char& GetScore(bool midterm) { return const_cast<char&>(const_cast<const Student*>(this)->GetScore(midterm)); } const char& GetScore(bool midterm) const { if (midterm) { return midtermScore; } else { return finalScore; } } private: char midtermScore; char finalScore; }; int main() { // كائن متغير Student a; هنا نستطيع الإسناد إلى المرجع، وتُستدعى نسخة متغيرة من GetScore، انظر بقية المثال: a.GetScore(true) = 'B'; a.GetScore(false) = 'A'; // كائن ثابت const Student b(a); لا زلنا نستطيع استدعاء تابع GetScore الخاص بكائن ثابت لأننا زدنا تحميل النسخة الثابتة منه -أي من GetScore-، انظر بقية المثال: std::cout << b.GetScore(true) << b.GetScore(false) << '\n'; } الدوال التوابع الثابتة (Const member functions) يمكن التصريح أنّ توابع صنف ما ثابتة (‎const‎)، وذلك سيخبر المصرّف والقارئ أنّ هذا التابع لن يعدّل الكائن، انظر: class MyClass { private: int myInt_; public: int myInt() const { return myInt_; } void setMyInt(int myInt) { myInt_ = myInt; } }; يكون مؤشر this في دالة التابع const من نوع ‎const MyClass *‎ وليس ‎MyClass *‎، وهذا يعني أننا لا نستطيع تغيير أي متغير عضو داخل الدالة وإلا سيعطي المصرفُ تحذيرًا، لذا لا يمكن التصريح بأن ‎setMyInt‎ ثابتة. كذلك يجب التصريح أنّ الدوال التوابع ثابتة const متى كان ذلك ممكنًا، ذلك أن التوابع الثابتة فقط هي التي يمكن أن تُستدعى على النوع ‎const MyClass‎. أيضًا لا يمكن التصريح عن التوابع الساكنة ‎static‎ أنها ثابتة const، ذلك أنّ التابع الساكن ينتمي إلى صنف ولا يستدعى على الكائن، فلا يمكنه تعديل المتغيّرات الداخلية للكائن، وعليه يكون التصريح عن توابع static على أنها const حشوًا لا فائدة منه. المتغيرات المحلية الثابتة تبين الشيفرة التالية كيفية التصريح عن متغيرات محلية ثابتة، وكيفية استخدامها، حيث يكون a من نوع const int فلا يمكن تغييره، وعليه لا يمكن تعيين قيمة جديدة إلى متغير ثابت. انظر: const int a = 15; a = 12; // خطأ، لا يمكن تعيين قيمة جديدة لمتغير ثابت a += 1; // خطأ، لا يمكن تعيين قيمة جديدة لمتغير ثابت جمع المراجع مع المؤشّرات: في الشيفرة التالية، لا يمكن ربط مرجع غير ثابت بمتغير ثابت كما ترى في السطر الأول، لكن في السطر الثاني يمكن ذلك بما أن c مرجع ثابت. انظر: int &b = a; const int &c = a; بالمثل، لا يمكن جمع مؤشر إلى قيمة متغيرة مع متغير ثابت، لكن يمكن ذلك كما ترى في السطر الثاني بما أن e مؤشر إلى قيمة ثابتة: int *d = &a; const int *e = &a يمكن ربط e بـ *int أو *const int بما أنها مؤشر غير ثابت إلى قيمة ثابتة: int f = 0; e = &f; e مؤشر إلى قيمة ثابتة، وذلك يعني أن القيمة التي تشير إليها لا يمكن تغييرها بتحصيل e: *e = 1 يمكن تغيير القيمة التالية من خلال تحصيل مؤشر إلى قيمة غير ثابتة: int *g = &f; *g = 1; المؤشرات الثابتة في المثال التالي، const int* pA = & a هو مؤشر إلى قيمة ثابتة، ولا يمكن تغيير قيمة a، بينما int* const pB = &a مؤشر ثابت، ويمكن تغيير قيمة a لكن لا يمكن تغيير قيمة المؤشر نفسه. انظر: int a = 0, b = 2; const int* pA = & a; int* const pB = &a; const int* const pC = &a; // مؤشر ثابت إلى قيمة ثابتة. // خطأ، لا يمكن التعيين إلى مرجع ثابت *pA = b; pA = &b; *pB = b; // خطأ، لا يمكن التعيين إلى مؤشر ثابت pB = &b; // خطأ، لا يمكن التعيين إلى مرجع ثابت *pC = b; // خطأ، لا يمكن التعيين إلى مؤشر ثابت pC = &b; asm تأخذ الكلمة المفتاحية ‎asm‎ مُعامَلًا واحدًا، وينبغي أن يكون سلسلة نصّية حرفيّة، ويتغيّر معناها بحسب التنفيذ ولكنها تُمرَّر عادةً إلى مُجمِّع التنفيذ (implementation's assembler)، مع دمج خَرْج المُجمِّع في وحدة الترجمة. التعليمة ‎asm‎ هي تعريفٌ وليست تعبيرًا، لذلك قد تظهر إمّا في نطاق كتلة (block scope) أو نطاق فضاء الاسم - namespace scope - بما في ذلك النطاق العام، ولكن قد لا تظهر ‎asm‎ داخل دالة تعبير ثابت ‎constexpr‎ نظرًا لتعذُّر تقييد التجميع المُضمّّن (inline assembly) بواسطة قواعد لغة C++‎. انظر المثال التالي: [ [noreturn] ] void halt_system() { asm("hlt"); } char تمثّل char نوعًا عدديًّا صحيحًا ذا حجم كبير بقدر يسمح بتخزين أي عضو من مجموعة محارف التطبيق الأساسية، ويُمكن أن يكون مؤشَّرًا - signed - (ولديه نطاق يتراوح بين -127 و +127، مضمنة) أو غير مؤشَّر - unsigned - (ولديه نطاق يتراوح بين 0 و 255 مُضمّنة). const char zero = '0'; const char one = zero + 1; const char newline = '\n'; std::cout << one << newline; // تطبع 1 متبوع بسطر جديد char16_t الإصدار ≥ C++‎ 11 تمثل char16_t نوعًا عدديًا صحيحًا غير مؤشَّر له نفس حجم ومحاذاة النوع ‎uint_least16_t‎، وعليه فهو كبير بما يكفي ليحتوي قيمةً من النوع UTF-16. const char16_t message[] = u"你好,世界\\n"; // مرحبا بالعالم بالصينية std::cout << sizeof(message)/sizeof(char16_t) << "\\n"; // 7 يطبع char32_t الإصدار ++ C++‎ 11 تمثّل char32_t نوعًا عدديًا صحيحًا غير مؤشّر له نفس حجم ومحاذاة ‎uint_least32_t‎، وعليه فهو كبير بما يكفي لِيحتوي قيمة من النوع UTF-32. const char32_t full_house[] = U"▯▯▯▯▯"; // محارف ليست من المستوى الأساسي متعدد اللغات // non-BMP characters std::cout << sizeof(full_house)/sizeof(char32_t) << "\\n"; // يطبع 6 int تمثّل int نوعًا عدديًا صحيحًا مؤشّرًا يختلف حجمه بحسب معماريّة بيئة التنفيذ، ويشمل نطاقه من -32767 إلى +32767 مُضمّنة. int x = 2; int y = 3; int z = x + y; يمكن دمج int مع ‎unsigned‎ و ‎short‎ و ‎long‎ و ‎long long‎ للحصول على أنواع عددية صحيحة أخرى. wchar_t تمثل wchar_t نوعًا عدديًا صحيحًا كبيرًا بما يكفي لتمثيل جميع الأحرف في مجموعة المحارف الممتدة (extended character set) المدعومة، ولا يمكن افتراض أن wchar_t تستخدم ترميزًا معينًا مثل UTF-16 لاختلاف طريقة تقديمها، وهي تُستخدم عادةً عندما تحتاج إلى تخزين أحرف من خارج ASCII لأنّ حجمها أكبر من ‎char‎. انظر المثال التالي الذي يعرض عبارة "مرحبا بالعالم\n" بعدة لغات: // الأمهرية const wchar_t message_ahmaric[] = L"▯▯▯ ▯▯▯ \\n"; // الصينية const wchar_t message_chinese[] = L"你好,世界\\n"; // العبرية const wchar_t message_hebrew[] = L"םלוע םולש\\n"; // الروسية const wchar_t message_russian[] = L"Привет мир\\n"; // التاميلية const wchar_t message_tamil[] = L"ஹலே◌◌ா உலகம◌்\\n"; float تمثّل float نوعًا من الأعداد العشرية، وهي أصغر أنواع الأعداد العشرية في C++‎. float area(float radius) { const float pi = 3.14159f; return pi * radius * radius; } double تمثّل double نوعًا من الأعداد العشرية، ويشمل نطاقها ‎float‎، وتشير عند دمجها مع ‎long‎ إلى النوع ‎long double‎ الذي يتضمّن نطاقه نطاقَ ‎double. double area(double radius) { const double pi = 3.141592653589793; return pi * radius * radius; } long تمثل long نوعًا عدديًّا صحيحًا مؤشَّرًا بطول يماثل ‎int‎ على الأقل، ويتضمن نطاقه المجال من -2147483647 إلى +2147483647 مُضمّنة (أي، من - (2 ^ 31 - 1) إلى + (2 ^ 31 - 1))، ويمكن كتابة هذا النوع أيضًا بالصيغة ‎long int‎. const long approx_seconds_per_year = 60L*60L*24L*365L; يشير التركيب ‎long double‎ إلى نوع عددي ذي فاصلة عائمة، والذي له النطاق الأوسع مقارنة بأنواع الأعداد العشرية الأخرى. long double area(long double radius) { const long double pi = 3.1415926535897932385L; return pi * radius * radius; } الإصدار ≥ C++‎ 11 عند تكرار ‎long‎ مرتين كما في ‎long long‎ فإنّها تشير إلى نوع عددي صحيح مؤشَّر طوله يساوي على الأقل طوال ‎long‎، ويشمل نطاقه على الأقل المجال من -9223372036854775807 إلى +9223372036854775807 مُضمّنة (أي، من - (2 ^ 63 - 1) إلى + (2 ^ 63 - 1)). // دعم أحجام ملفات تصل إلى 2 تيرا بايت const long long max_file_size = 2LL << 40; short تشير short إلى نوع عددي صحيح مؤشَّر طوله يساوي ‎char‎ على الأقل، ويتضمن نطاقه المجال من -32767 إلى +32767 مُضمّنة، ويمكن كتابة هذا النوع أيضًا هكذا ‎short int‎. // خلال السنة الماضية short hours_worked(short days_worked) { return 8 * days_worked; } bool تمثّل bool نوعًا عدديًا صحيحًا يمكن أن تكون قيمته إما ‎true‎ أو ‎false‎. bool is_even(int x) { return x % 2 == 0; } const bool b = is_even(47); // false signed signed هي كلمة مفتاحية تكون جزءًا من أسماء بعض الأنواع العددية الصحيحة. عند استخدامها بمفردها، تعيد النوع ‎int‎ ضمنيًا، وفي هذه الحالة فإنّ الأنواع ‎signed‎ و ‎signed int‎ و ‎int‎ متماثلة. عند دمجها مع ‎char‎ تعيد النوع ‎signed char‎، وهو نوع مختلف عن ‎char‎، حتى لو كانت ‎char‎ مؤشّرة، كما يحتوي نطاق ‎signed char‎ بين -127 إلى +127 مضمّنة على الأقل. عند دمجها مع ‎short‎ أو ‎long‎ أو ‎long long‎، فستكون مجرّد تكرار، لأنّ هذه الأنواع مؤشَّرة سلفًا. لا يمكن دمج ‎signed‎ مع ‎bool‎ أو ‎wchar_t‎ أو ‎char16_t‎ أو ‎char32_t‎. مثال: signed char celsius_temperature; std::cin >> celsius_temperature; if (celsius_temperature < -35) { std::cout << "cold day, eh?\n"; } unsigned unsigned هي مُحدِّد نوع يتطلب النسخة غير المؤشّرة (unsigned) من نوع عددي صحيح. عند استخدامها بمفردها، فإنّها تعيد النوع ‎int‎ ضمنيًا،، وعليه يتماثل كل من ‎unsigned‎ و ‎unsigned int‎ في النوع. يختلف النوع ‎unsigned char‎ عن ‎char‎ حتى لو كان الأخير غير مؤشّر، ويمكن استخدامه لتمثيل الأعداد الصحيحة الأصغر من 255. يمكن أيضًا دمج ‎unsigned‎ مع ‎short‎ أو ‎long‎ أو ‎long‎ ‎long‎. ولا يمكن دمجها مع ‎bool‎ أو ‎wchar_t‎ أو ‎char16_t‎ أو ‎char32_t‎. مثال: char invert_case_table[256] = { ..., 'a', 'b', 'c', ..., 'A', 'B', 'C', ... }; char invert_case(char c) { unsigned char index = c; return invert_case_table[index]; // مباشرة invert_case_table[c] إعادة // مؤشّرة char غير مناسب إن كانت } لاحظ في المثال السابق أن إعادة [invert_case_table[c غير مناسب إن كانت char مؤشَّرة. كلمات النوع المفتاحية class تشمل استخدامات كلمة class المفتاحية ما يلي: تقديم تعريف نوع الصنف: class foo { int x; public: int get_x();f void set_x(int new_x); }; تقديم مُحدِّد مفصّل للنوع يشير إلى أنّ الاسم التالي هو اسم لنوع صنف (class type)، وإن كان اسم الصنف قد صُرِّح عنه سلفًا فيمكن العثور عليه حتى لو كان مخفيًا باسم آخر، أما إن لم يكن قد صُرح عنه فسيُعدّ مُصرّحًا عنه بشكل لاحق (forward-declared). class foo; // مُحدِّد مفصّل للنوع -> تصريح لاحق class bar { public: bar(foo & f); }; void baz(); لدينا في الشيفرة التالية محدِّد مفصَّل للنوع، وتصريح لاحق آخر، لاحظ أن الصنف ودالة ()void baz لهما نفس الاسم: class baz; class foo { bar b; في الشيفرة التالية، مُحدِّد نوع مفصّل يشير إلى الصنف وليس إلى الدالة التي لها نفس الاسم: friend class baz; public: foo(); }; تقديم نوع المعاملات في تصريح القالب. template < class T > const T& min(const T& x, const T& y) { return b < a ? b : a; } في التصريح عن معامل قالب القالب (template template parameter)، تسبق الكلمة المفتاحيّة ‎class‎ اسمَ المعامل، ونظرًا لأنّ الوسيط الخاص بمعامل قالب القالب لا يمكن أن يكون إلّا قالب صنف (class template)، فلا حاجة لاستخدام ‎class‎ هنا، لكن رغم هذا فإنّ لغة C++‎ تُوجِبه. لاحظ في المثال التالي كيف تُستخدم class الأخيرة في الصف الأول، حيث يكون U هو معامل قالب القالب. template < template < class T > class U > void f() { U < int > ::do_it(); U < double > ::do_it(); } لاحظ أنّه يمكن الجمع بين النقطتين الثانية والثالثة في نفس التصريح، انظر المثال التالي حيث لا يُشترط أن تكون bar قد ظهرت من قبل: template < class T > class foo {}; foo < class bar > x; الإصدار ≥ C++‎ 11. في تصريح أو تعريف نوع عددي enum، تصرّح أن التعداد هو تعدادٌ نطاقي (scoped enum): enum class Format { TEXT, PDF, OTHER, }; Format f = F::TEXT; enum هناك عدة أدوار للكلمة المفتاحية enum، وهي كالتالي: تقديم تعريف لنوع عددي. enum Direction { UP, LEFT, DOWN, RIGHT }; Direction d = UP; الإصدار ≥ C++‎ 11 قد تُلحَق ‎enum‎ اختياريًا في الإصدار C++‎ 11 بكلمة ‎class‎ المفتاحية أو ‎struct‎ من أجل تعريف نوع تعداد نطاقي، ويمكن تحديد النوع الأساسي (underlying type) لكلٍّ من الأنواع العددية النطاقية وغير النطاقية عبر وضع ‎: T‎ بعد اسم النوع العددي، إذ تشير ‎T‎ إلى نوع عددي صحيح. enum class Format : char { TEXT, PDF, OTHER }; Format f = Format::TEXT; enum Language : int { ENGLISH, FRENCH, OTHER }; كذلك يمكن أن تُسبق العدّادات (Enumerators) في أنواع ‎enum‎ العاديّة بعامِل النطاق (scope operator). لكن رغم ذلك ستُعدّ ضمن النطاق الذي عُرِّف فيه ‎enum‎. Language l1, l2; l1 = ENGLISH; l2 = Language::OTHER; تقديم مُحدّد مفصَّل للنوع يشير إلى أنّ الاسم التالي هو اسم نوع عددي سبق التصريح عنه. (لا يمكن استخدام مُحدّد مفصَّل للنوع في تصريح لاحق لنوع عددي)، ويمكن تسمية نوع عددي بهذه الطريقة حتى لو كان يخفيه اسم آخر. انظر في المثال التالي كيف أن صيغة Foo foo = FOO غير صحيح لأن Foo تشير إلى الدالة، بينما enum Foo foo = FOO صحيحة لأنها تشير إلى النوع العددي. enum Foo { FOO }; void Foo() {} Foo foo = FOO; enum Foo foo = FOO; الإصدار ≥ C++‎ 11 تقديم تصريح عن نوع عددي مبهم يصرّح عن نوع عددي لكن دون تعريفه، وقد يعيد التصريح عن نوع عددي سبق التصريح عنه، ويمكن كذلك أن يُصرِّح لاحقًا (forward declaration) عن نوع عدديّ لم يسبق التصريح عنه. لكن من الناحية الأخرى، لا يمكن لنوع سبق التصريح عنه كنوع نطاقي أن يُعاد التصريح عنه كنوع عددي وتحويله إلى نوع غير نطاقي، والعكس صحيح. ويجب أن تتوافق جميع تصريحات النوع العددي على مستوى النوع الأساسي (underlying type). كذلك يجب تحديد النوع الأساسي بشكل صريح عند التصريح اللاحق عن نوع عددي غير نطاقي،ذلك أنه لا يمكن أن يُستنتج إلى حين التعرّف على قيم العدّادات. انظر المثال التالي حيث يكون النوع الأساسي هو int: enum class Format; void f(Format f); enum class Format { TEXT, PDF, OTHER, }; enum Direction; // غير صالح، يجب تحديد النوع الأساسي struct الكلمة المفتاحية struct مشابهة للكلمة ‎class‎، بيد أنّ هناك بعض الاختلافات، وهي: في حال تعريف نوعِ صنفٍ باستخدام الكلمة المفتاحية ‎struct‎ فستكون صلاحيات الوصول الافتراضية للأصناف الأساسية (bases) والأعضاءِ عامةً أي ‎public‎ وليس ‎private‎. لا يمكن استخدام ‎struct‎ للتصريح عن معامل نوع قالب (template type parameter) أو قالب معامِل القالب بل ‎class‎ فقط هو من يمكنه ذلك. union هناك عدة أدوار للكلمة المفتاحية union، وهي كالتالي: تقديم تعريف نوع الاتحاد (union type) : // POSIX مثال من union sigval { int sival_int; void *sival_ptr; }; تقديم محدِّد مفصّل للنوع يشير إلى أنّ الاسم التالي هو اسم لنوع اتحاد، وإذا صُرِّح عن اسم الاتحاد سلفًا فيمكن العثور عليه حتى لو كان يخفيه اسم آخر، وإلا فسيكون مُصرَّحا عنه بشكل لاحق (forward-declared): union foo; // محدِّد مفصّل للنوع -> تصريح اللاحق. class bar { public: bar(foo & f); }; void baz(); union baz; // baz() الصنف له نفس اسم الدالة union foo { long l; union baz * b; // محدّد نوع مفصل يشير إلى الصنف وليس الدالة ذات الاسم نفسه }; الكلمة المفتاحية mutable تعابير لامدا القابلة للتغيير يُمنع إجراء العمليات غير الثابتة non-const على تعابير لامدا (λ) بسبب كون العامل الضمني ‎operator()‎ ثابتًا (const)، ويمكن التصريح عن تعبير (λ) كقيمة قابلة للتغير لجعل ‎operator()‎ غير ثابت (non-const)، ومن ثم يمكننا التعديل على الأعضاء. انظر المثال التالي حيث نحصل على خطأ لأن ‎operator()‎ ثابت فلا يمكننا تعديل الأعضاء. int a = 0; auto bad_counter = [a] { return a++; }; لاحظ الآن كيف يمكننا تعديل الأعضاء بعد جعل تعبير لامدا قابلًا للتغيير: auto good_counter = [a]() mutable { return a++; // OK } good_counter(); // 0 good_counter(); // 1 good_counter(); // 2 معدِّلات الصنف المتغيرة تُستخدَم المُعدِّلات القابلة للتغيير ‎mutable‎ في هذا السياق للإشارة إلى إمكانية تعديل حقل كائن ثابت دون التأثير على حالة الكائن المرئية من الخارج، فيفضَّل استخدام كلمة mutable المفتاحية إن كنت تريد أن تخزّن نتيجةَ عمليّات حسابية مكلّفة وطويلة بشكل مؤقتٍ (caching). وإذا كان لديك حقل مقفول - lock data field - (مثال: ‎std::unique_lock‎)، والذي يُقفَل ويُفتَح داخل تابع ثابت، فستكون هذه الكلمة المفتاحية مفيدة في هذه الحالة كذلك. لكن لا تستخدم هذه الكلمة لإلغاء ثباتيّة (const-ness) كائن، انظرالمثال التالي: class pi_calculator { public: double get_pi() const { if (pi_calculated) { return pi; } else { double new_pi = 0; for (int i = 0; i < 1000000000; ++i) { // new_pi بعض الحسابات لتحسين } pi = new_pi; pi_calculated = true; return pi; } } private: mutable bool pi_calculated = false; mutable double pi = 0; }; لاحظ أنه في الشيفرة السابقة، إن كان كل من pi و pi_calculated لا يقبلان التعديل فسنحصل على خطأ من المصرِّف إذ لا يمكن تعديل حقل غير قابل للتغيير في تابع ثابت. كلمات مفتاحية أخرى void تمثل void نوعًا فارغًا غير مكتمل، ويستحيل لكائن أن يكون من النوع ‎void‎، ولا مصفوفات من هذا النوع ولا حتى مرجع إليه، وإنّما يُستخدم كنوع إعادة للدوال التي لا تُعيد أيّ شيء. عند استخدام void كنوع للقيمة المعادة من دالة فإنّها تشير إلى أنّ تلك الدالة لا تُعيد أيّ قيمة، وعند استخدامها في قائمة معاملاتِ دالةٍ فإنها تدلّ على أنّ تلك الدالة لا تأخذ أيّ معاملات (مثلًا: ‎int main()‎ و ‎int main(void)‎ متكافئتان) وأُجيزَت هذه الصياغة للتوافق مع C حيث تصريحات الدوال لها معنى مختلف عن مثيلاتها C++‎. أما عند استخدامها في تصريح مؤشّر فذلك يعني أنّ ذلك المؤشّر عام (universal). إذا كان المؤشّر من النوع void *‎ فإنّه يستطيع الإشارة إلى أيّ متغير لم يُصرّح عنه باستخدام const أو volatile، ولا يمكن تحصيل (dereferencing) مؤشّر فارغ (من النوع void *‎) إلا إن حُوِّل إلى نوع آخر. كذلك يمكن تحويل مؤشر فارغ إلى أيّ نوع آخر من مؤشّرات البيانات. وهذه الميزة تجعل النوع ‎void*‎ مناسبًا لأنواع معيّنة من واجهات مسح الأنواع (type-erasing interfaces) غير الآمنة (type-unsafe)، مثل السياقات العامة في الواجهات البرمجية (API) الشبيهة بـC مثل ‎qsort‎ و ‎pthread_create‎. يمكن أن يشير المؤشّر الفارغ إلى دالة، ولكن ليس إلى عضو من صنف في C++‎. void vobject; // C2182 void *pv; // okay int *pint; int i; int main() { pv = &i; // C++ لكنه ضروري في C التحويل اختياري في pint = (int * ) pv; يمكن تحويل أيّ تعبير ليكون من نوع ‎void‎، وهو ما يسمى تعبير القيمة المهملة (discarded-value expression): static_cast<void>(std::printf("Hello, %s!\n", name)); // إهمال القيمة المعادة قد يكون هذا مفيدًا للإشارة إلى أنّ قيمة التعبير ليست مهمّة، وأنّ التعبير يجب تقييمه لأجل آثاره الجانبية فقط. Volatile الكلمة المفتاحية Volatile هي مؤهِّلُ نوع (type qualifier) يمكن استخدامه للتصريح بأنّ كائنًا ما قابلٌ لأن يُغيَّر في البرنامج من قِبل عتاد الحاسوب وتنتج النسخة المتغيرة (volatile-qualified) من نوع عند تطبيقها عليه، ويلعب التأهيل المتغير (Volatile qualification) نفس الدور الذي تلعبه ‎const‎ في نظام الأنواع، لكن ‎volatile‎ لا تمنع تعديل الكائنات، بل تفرض على المُصرّف معاملة عمليّات الوصول إلى هذه الكائنات كآثار جانبية. volatile declarator ; في المثال أدناه، إذا لم تكن ‎memory_mapped_port‎ متغيّرة (Volatile) فيمكن للمصرّف تحسين الدالة بحيث لا ينفّذُ إلا الكتابة النهائية، وسيكون ذلك غير صحيح إذا كانت ‎sizeof(int)‎ أكبر من 1، ويجبر التأهيل المتغير ‎volatile‎ المُصرّفَ على معاملة عمليات كتابة ‎sizeof(int)‎ على أنها تأثيرَات جانبية، وعليه ينفّذها جميعهًا بالترتيب. extern volatile char memory_mapped_port; void write_to_device(int x) { const char* p = reinterpret_cast < const char * > ( &x); for (int i = 0; i < sizeof(int); i++) { memory_mapped_port = p[i]; } } virtual تصرح الكلمة المفتاحية virtual عن دالة وهمية، أو عن صنف أساسي وهمي (virtual base class). virtual [type-specifiers] member-function-declarator virtual [access-specifier] base-class-name المعامِلات type-specifiers: تحدّد نوع القيمة المعادة من دالة التابع الوهمي. member-function-declarator: تصرّح عن دالة تابع. access-specifier: تحدّد مستوى الوصول إلى الصنف الأساسي: عام (public) أو محمي (protected ) أو خاص (private)، ويمكن أن تظهر قبل أو بعد الكلمة المفتاحية virtual. base-class-name: تعرّف نوع صنف سبق تعريفه. مؤشر this this هو مؤشّر يمكن الوصول إليه فقط داخل دوال التوابع المتغيرة (non static) لنوع أو صنف أو اتحاد أو بِنية. ويشير إلى الكائن الذي استُدعِي عليه التابع، ولا تملك دوال التوابع الساكنة مؤشر this. this->member-identifier المؤشّر this ليس جزءًا من الكائن نفسه ولا يُحسب في تعليمة الحجم sizeof للكائن، بل يُمرَّر عنوان ذلك الكائن من قبل المُصرِّف كوسيط مخفي إلى الدالة عند استدعاء دالة تابع غير ساكن على كائن ما. انظر المثال التالي: myDate.setMonth( 3 ); يمكن تأويله بهذه الطريقة: setMonth( &myDate, 3 ); معظم استخدامات thisضمنيّة، ويُسمح باستخدام this صراحة عند الرجوع إلى أعضاء الصنف رغم أنّ ذلك غير ضروري. انظر: void Date::setMonth(int mn) { month = mn; // هذه العبارات الثلاث متكافئة this-> month = mn; ( *this).month = mn; } يُستخدم التعبير ‎*this لإعادة الكائن الحالي من دالة تابع، ويُستخدم هذا المؤشّر أيضًا لمنع التنكيس أو الإشارة الذاتية ( self-reference): if ( &Object != this) { // لا تنفّذ في حال الإشارة الذاتية. عبارات try و throw و catch تُستخدم عبارات try و throw و catch لمُعالجة الاعتراضات (Exceptions) في C++‎، أولًا، استخدم كتلة try لتضع فيها تعليمة أو أكثر من التي قد تطلق اعتراضًا. يشير تعبير throw إلى أنّ شيئًا اعتراضيًا قد حدث في كتلة try -غالبًا ما يكون خطأ-، ويمكنك استخدام كائن من أيّ نوع كمعامَل لـ throw، ويُستخدم هذا الكائن لتوصيل معلومات بخصوص الخطأ. ويوصى في معظم الحالات باستخدام الصنف std::exception أو أحد الأصناف المشتقة منه والمُعرّفة في المكتبة القياسية، أما إذا لم تكن تلك الاستثناءات مناسبة فيمكنك اشتقاق اعتراض خاص بك من std::exception. استخدم كتلةَ catch واحدة أو أكثر بعد كتلة try لمعالجة الاعتراضات التي يمكن إطلاقها، إذ تحدّد كل كتلة من كتل catch نوعًا من الاستثناءات التي يمكنها معالجتها. انظر: MyData md; try { // شيفرة قد تؤدّي إلى إطلاق اعتراض md = GetNetworkResource(); } catch (const networkIOException& e) { // شيفرة ستُنفّذ في حال إطلاق اعتراض من نوع // networkIOException // try في كتلة ... // عرض رسالة الخطأ الخاصة بالاعتراض cerr << e.what(); } catch (const myDataFormatException & e) { // شيفرة تعالج بقيّة أنواع الاعتراضات // ... cerr << e.what(); } // throw الصيغة التالية تُظهر عبارة MyData GetNetworkResource() { // ... if (IOSuccess == false) throw networkIOException("Unable to connect"); // ... if (readError) throw myDataFormatException("Format error"); // ... } الشيفرة التي تلي try هي الجزء المَحمِيّ من الشيفرة، ويطلق التعبير throw اعتراضًا، أمّا كتلة التعليمات البرمجية الموضوعة بعد catch فهي المسؤولة عن إمساك ومعالجة الاعتراض الذي أُطلق إذا توافقت الأنواع في تعبيرات throw و catch. try { throw CSomeOtherException(); } catch (...) { // إمساك كل الاستثناءات // معالجة الاستثناء جزئيا، ثم إعادة إطلاق الاستثناء ليُعالِجه معالِجٌ آخر // ... throw; } friend تغلف الأصناف المصممة جيدًا وظائفها لتخفي تفاصيلها في نفس الوقت الذي تقدم فيه واجهة نظيفة وبسيطة وموثقة جيدًا، وذلك يسهِّل التعديل أو إعادة التصميم طالما ظلت الواجهة كما هي. لكن قد تحتاج بعض الأصناف في الحالات الأكثر تعقيدًا إلى معرفة تفاصيل تطبيق بعضها البعض، ويتيح مفهوم الأصناف والدّوال الصديقة (Friend classes and functions) لها الوصول إلى تلك التفاصيل دون المساس بتغليف المعلومات. يكون من المفيد أحيانًا أن تمنح حقّ الوصول لأعضاءِ صنفٍ ما إلى دالةٍ ليست من أعضاء ذلك الصنف، أو إلى جميع الأعضاء في صنف آخر، ولا يمكن إلّا لمنفِّذ الصنف أن يصرح عن أصدقائه، ولا يمكن أن يصرح صنف أنه صديق لصنف آخر، وبالمثل لا يمكن لدالة أن تصرح عن ذلك. استخدام كلمة friend واسم دالة غير عضو أو اسم صنف آخر في تعريف الصنف، وذلك لمنحه حق الوصول إلى الأعضاء الخاصّين والمحميّين في ذلك الصنف، كما يمكن التصريح عن معامِل نوع كصديق في تعريف قالب ما. إذا صرحت عن دالة صديقة لم يسبق التصريح عنها، فستُصدَّر تلك الدالة إلى النطاق المحيط غير الصنفي (enclosing nonclass scope). class friend F friend F; class ForwardDeclared; // اسم الصنف معروف class HasFriends { friend int ForwardDeclared::IsAFriend(); // C2039 خطأ }; الدوال الصديقة (friend functions) الدالة الصديقة هي دالة ليست عضوًا في صنف، ولكن لها حق الوصول إلى الأعضاء المحميّين والخواصّ في ذلك الصنف، ولا تُعدّ الدوال الصديقة من أعضاء الصنف بل هي دوال خارجية طبيعية تُمنح امتيازات وصول خاصّة. لا تدخل الدوال الصديقة في نطاق الصنف ولا تُستدعى باستخدام عوامل اختيار الأعضاء . و ‎->‎ إلّا إن كانت أعضاء من صنف آخر. يُصرَّح عن الدالّة الصديقة من قِبل الصنف الذي يمنح حق الوصول، ويمكن وضع تصريح الصداقة في أيّ مكان في تصريح الصنف، ولا يتأثّر التصريح بالكلمات المفتاحيّة الخاصّة المسؤولة عن التحكم في الوصول. #include <iostream> using namespace std; class Point { friend void ChangePrivate(Point & ); public: Point(void): m_i(0) {} void PrintPrivate(void) { cout << m_i << endl; } private: int m_i; }; void ChangePrivate (Point &i) { i.m_i++; } int main() { Point sPoint; sPoint.PrintPrivate(); ChangePrivate(sPoint); sPoint.PrintPrivate(); // الخرج 0 1 } تستطيع الأصناف والبُنى أن تصرح عن أيّ دالة على أنها صديقة لها، وإذا كانت دالة ما صديقة لصنف معيّن، فإنها تصل إلى جميع أعضائه المحميّين (protected) والخواص (private)، انظر: // تصريح مسبق للدوال void friend_function(); void non_friend_function(); class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} private: int private_value; // إعلان إحدى الدوال صديقة friend void friend_function(); }; void non_friend_function() { PrivateHolder ph(10); // Compilation error: private_value is private. std::cout << ph.private_value << std::endl; } void friend_function() { // يُسمح للأصدقاء بالدخول إلى القيم الخاصة PrivateHolder ph(10); std::cout << ph.private_value << std::endl; } مُعدِّلات الوصول لا تغيّر الدلالات الصديقة، وستكون الأعضاء العامة والمحمية والخاصّة لصنفٍ صديقٍ متكافئة كلها. كذلك لا تُورّث تصريحات الصداقة (Friend declarations)، فإن أنشأنا مثلًا صنفًا فرعيا من ‎PrivateHolder‎ … : class PrivateHolderDerived: public PrivateHolder { public: PrivateHolderDerived(int val): PrivateHolder(val) {} private: int derived_private_value = 0; }; … ثم حاولنا الوصول إلى أعضائه، سنحصل على ما يلي: void friend_function() { PrivateHolderDerived pd(20); // OK std::cout << pd.private_value << std::endl; // Compilation error: derived_private_value is private. std::cout << pd.derived_private_value << std::endl; } لاحظ أنّ دالة التابع ‎PrivateHolderDerived‎ لا يمكنها الوصول إلى ‎PrivateHolder::private_value‎، في حين أنّ الدوال الصديقة تستطيع ذلك. التوابع الصديقة يمكن جعل التوابع صديقة تمامًا مثل الدوال، انظر: class Accesser { public: void private_accesser(); }; class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} friend void Accesser::private_accesser(); private: int private_value; }; void Accesser::private_accesser() { PrivateHolder ph(10); // الإعلان عن هذا التابع كصديق std::cout << ph.private_value << std::endl; } الصنف الصديق (Friend class) يمكن الإعلان عن صنف بأكمله كصديق، ويعني ذلك أنّه يجوز لأيّ عضو من أعضاء الصنف الصديق الوصول إلى الأعضاء الخاصّة والمحمية للصنف الآخر، انظر: class Accesser { public: void private_accesser1(); void private_accesser2(); }; class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} friend class Accesser; private: int private_value; }; void Accesser::private_accesser1() { PrivateHolder ph(10); // OK std::cout << ph.private_value << std::endl; } void Accesser::private_accesser2() { PrivateHolder ph(10); // OK std::cout << ph.private_value + 1 << std::endl; } لا يمكن عكس التصريح بالصداقة بين الأصناف، فإن كان صنف A صديقًا لصنف B، فإنّ B لن يصبح تلقائيًّا صديقًا للصنف A. وإذا احتاجت الأصناف وصولًا خاصًا في كلا الاتجاهين فسيحتاجان كليهما إلى تصريحات صداقة. انظر: class Accesser { public: void private_accesser1(); void private_accesser2(); private: int private_value = 0; }; class PrivateHolder { public: PrivateHolder(int val): private_value(val) {} friend class Accesser; void reverse_accesse() { Accesser a; std::cout << a.private_value; } private: int private_value; }; في المثال السابق، لاحظ كيف أن Accessor صديق للصنف PrivateHolder، لكن الأخير لا يستطيع الوصول إلى أعضاء الأول. مصادقة أعضاء صنف يوضّح المثال التالي كيفيّة مُصادَقة أعضاء صنف ما، حيث تكون A::Func1 دالة صديقة للصنف B، لذا يمكنها الدخول إلى جميع أعضاء B. class B; class A { public: int Func1(B& b); private: int Func2(B& b); }; class B { private: int _b; friend int A::Func1(B& ); }; int A::Func1(B& b) { return b._b; } // OK int A::Func2(B& b) { return b._b; } // C2248 typename عند إلحاق اسم مؤهَّل بكلمة ‎typename‎ فإنها تشير إلى أنها اسم لنوع، وغالبًا ما يكون هذا ضروريًا في القوالب، خاصة عندما يكون مُحدِّد الاسم المتشعّب (nested name specifier) نوعًا غير مستقل يخالف الاستنساخ الحالي. انظر المثال التالي حيث يعتمد ‎std::decay<T>‎ على معامل القالب ‎T‎، فنحتاج إلى إسباق الاسم المؤهَّل كله بكلمة typename المفتاحية من أجل تسمية النوع المتشعّب ‎type‎. راجع هذا الرابط لتعرف أين تضع كلمات template وtypename وأين أيضًا: template <class T> auto decay_copy(T&& r) -> typename std::decay<T>::type; تقدّم ‎typename‎ معامِلَ نوعٍ (type parameter) في تصريح القالب، وهي تماثل class في هذا السياق. template < typename T > const T& min(const T& x, const T & y) { return b < a ? b : a; } الإصدار ≥ C++‎ 17 يمكن أيضًا استخدام ‎typename‎ عند التصريح عن معامِل قالب القالب (template template parameter)، سابقة بهذا اسم المعامل تمامًا مثل ‎class‎. template < template < class T > typename U > void f() { U<int>::do_it(); U<double>::do_it(); } explicit عند تطبيق الكلمة المفتاحية explicit على مُنشئ ذي وسيط واحد، فإنها تمنع أن يُستخدَم ذلك المُنشئ لإجراء أيّ تحويلات ضمنية. class MyVector { public: explicit MyVector(uint64_t size); }; MyVector v1(100); // OK uint64_t len1 = 100; MyVector v2 { len1 }; // uint64_t من النوع len1 int len2 = 100; MyVector v3 { len2 }; // uint64_t إلى int غير مسموح لأنه تحويل ضمني من النوع منذ إدخال قوائم المهيئات (initializer lists) في C++‎ 11، صار بالإمكان تطبيق ‎explicit‎ على أيّ مُنشئ بغضّ النظر عن عدد وسائطه. انظر: struct S { explicit S(int x, int y); }; S f() { return {12, 34}; // صيغة غير صحيحة. return S{12, 34}; // ok } الإصدار ≥ C++‎ 11 عندما تُطبّق على دالّة تحويل (conversion function)، فإنّها تمنع استخدام تلك الدالّة لإجراء أيّ تحويلات ضمنية. class C { const int x; public: C(int x) : x(x) {} explicit operator int() { return x; } }; C c(42); int x = c; // غير صحيح. int y = static_cast<int> (c); // صيغة صحيحة لأنه تحويل صريح. sizeof sizeof هو عامل أحادي يعيد حجم معامَله بالبايت، وقد يكون يكون ذلك المعامَل تعبيرًا أو نوعًا، وإذا كان المعامَل تعبيرًا فلن يُقيَّم، وسيكون الحجم تعبيرًا ثابتًا من النوع ‎std::size_t‎. أما إن كان العامل نوعًا، فيجب أن يوضع بين قوسين. لا يجوز تطبيق ‎sizeof‎ على نوع دالّة (function type). لا يجوز تطبيق ‎sizeof‎ على نوع غير مكتمل، بما في ذلك ‎void‎. إذا طُبِّق sizeof على نوع مرجعي مثل ‎T&‎ أو ‎T&&‎، فسيكون مكافئًا لـ ‎sizeof(T)‎. عندما تُطبّق ‎sizeof‎ على نوع صنف فإنها تعيد عدد البايتات في كائن كامل من ذلك النوع، بما في ذلك أيّ بايت للحشو (padding bytes) في المنتصف أو في النهاية. لذا لا تساوي ‎sizeof‎ القيمة صفر أبدًا. حجم كل من النوع ‎char‎ و ‎signed char‎ و ‎unsigned char‎ يساوي واحد، لكن تذكّر أنّ البايت هو مقدار الذاكرة المطلوبة لتخزين كائن من النوع ‎char‎، وهذا لا يعني بالضرورة أنه يساوي 8 بتّات (bits) لأنّ بعض الأنظمة تخزِّن كائنات ‎char‎ في مساحة أكبر من 8 بتات. كذلك، إذا كان expr تعبيرًا، فإنّ ‎sizeof(‎ expr ‎)‎ تكافئ ‎sizeof(T)‎، حيث يمثّل ‎T‎ نوع التعبير expr. int a[100]; std::cout << "The number of bytes in `a` is: " << sizeof a; memset(a, 0, sizeof a); الإصدار ≥ C++‎ 11 يعيد العامل ‎sizeof...‎ عدد العناصر في حُزمة معامِلات ما. template < class...T > void f(T && ...) { std::cout << "f was called with " << sizeof...(T) << " arguments\n"; } noexcept الإصدار ≥ C++‎ 11 noexcept هو معامل أحاديٌّ يحدّد إمكانية نشرُ اعتراض كنتيجة لتقييم عامِله، لاحظ أنّ متون الدوال المُستدعاة لا تُفحَص، وعليه فقد تؤدي ‎noexcept‎ إلى نتائج غير متوقّعة، بالمثل فإن العامل لا يُقيَّم. #include <iostream> #include <stdexcept> void foo() { throw std::runtime_error("oops"); } void bar() {} struct S {}; int main() { std::cout << noexcept(foo()) << '\n'; // يطبع صفرًا std::cout << noexcept(bar()) << '\n'; // يطبع صفرًا std::cout << noexcept(1 + 1) << '\n'; // يطبع واحدًا std::cout << noexcept(S()) << '\n'; // يطبع واحدًا } رغم أنّ ‎bar()‎ في المثال السابق لن تطلق أيّ اعتراض إلا أنّ ‎noexcept(bar())‎ تبقى خاطئة (false)، والسبب أنّه لم يُحدَّد بشكل صريح أنّ ‎bar()‎ غير قادرة على نشر اعتراض. تحدّد noexcept عند التصريح عن دالّة إن كانت تلك الدالّة قادرة على نشر اعتراض أم لا، وإذا استُخدِمت وحدَها فإنها تعلن أنّ الدالة لا يمكنها نشر أيّ اعتراض، وإذا استُخدِمت مع وسيط موضوع بين قوسين فإنها تحدّد إن كانت الدالّة تستطيع أن تنشر اعتراض اعتمادًا على قيمة الوسيط الحقيقية. void f1() { throw std::runtime_error("oops"); } void f2() noexcept(false) { throw std::runtime_error("oops"); } void f3() {} void f4() noexcept {} void f5() noexcept(true) {} void f6() noexcept { try { f1(); } catch (const std::runtime_error&) {} } في هذا المثال، صرّحنا بأنّ ‎f4‎ و ‎f5‎ و ‎f6‎ لا يمكنها نشر الاعتراضات (Exceptions) -رغم أنّه يمكن إطلاق اعتراض أثناء تنفيذ ‎f6‎، إلا أنّه سيُحصَر ولن يُسمح له بالانتشار خارج الدالّة-، وصرحنا كذلك أن ‎f2‎ تستطيع نشر اعتراض. ويكافئ حذف مُحدِّدات ‎noexcept‎ التعبيرَ ‎noexcept(false)‎‎، لذلك فقد صرحنا ضمنيًا أنّ f1 و f3 يمكنهما نشر الاعتراضات رغم أنّه لا يمكن إطلاق الاعتراضات أثناء تنفيذ ‎f3‎. الإصدار ≥ C++‎ 17 تتوقف قدرة دالة ما في حصر الاعتراضات (‎noexcept‎) على نوع تلك الدالة، ففي المثال أعلاه، تختلف أنواع ‎f1‎ و ‎f2‎ و ‎f3‎ عن ‎f4‎ و ‎f5‎ و ‎f6‎، لذلك فإنّ ‎noexcept‎ مهمّة في مؤشّرات الدوالّ ووسائط القوالب وغير ذلك. void g1() {} void g2() noexcept {} void( *p1)() noexcept = &g1; // غير صالح void( *p2)() noexcept = &g2; // تطابق الأنواع void( *p3)() = &g1; // تطابق الأنواع void( *p4)() = &g2; // تحويل ضمني هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 23: Keywords من كتاب C++ Notes for Professionals.
  5. تشير البرمجة الوصفية (Metaprogramming) في C++‎ إلى استخدام وحدات الماكرو أو القوالب لتوليد شيفرة أثناء وقت التصريف (compile)، ويُفضّل استخدام القوالب بدلًا من وحدات الماكرو رغم أنّها أقلّ شمولية منها. وغالبًا ما تستفيد البرمجة الوصفية للقوالب من الحسابات أثناء وقت التصريف (compile-time)، سواء عبر القوالب أو عبر دوال ‎constexpr‎ لتوليد الشيفرة البرمجية. تجدر الإشارة إلى أنّ حسابات وقت التصريف لا تُصنّف على أنّها من البرمجة الوصفية. حساب المضروب (Calculating Factorial) يمكن حساب المضروب أثناء التصريف باستخدام تقنيّات البرمجة الوصفية للقوالب (template metaprogramming)، انظر: #include <iostream> template < unsigned int n > struct factorial { enum { value = n * factorial < n - 1 > ::value }; }; template < > struct factorial < 0 > { enum { value = 1 }; }; int main() { std::cout << factorial < 7 > ::value << std::endl; // "5040" طباعة } المضروب ‎factorial‎ هو بُنية (struct)، بيْد أنّ البرمجة الوصفية للقوالب تُعامله على أنّه دالة قالب وصفية (template metafunction). اصطلاحًا، تُقيَّم دوال القالب الوصفية عن طريق التحقق من عضو معيّن، إمّا ‎::type‎ بالنسبة للدوال الوصفية التي تعيد أنواعًا، أو ‎::value‎ بالنسبة للدوال الوصفية التي تعيد قيمًا. وفي الشيفرة أعلاه، نقيِّم الدالة الوصفية ‎factorial‎ عن طريق استنساخ (instantiating) القالب باستخدام المعامِلات التي نريد تمريرها، ونستخدم ‎::value‎ للحصول على نتيجة التقييم. تعتمد الدالة الوصفية على استنساخ نفس الدالة الوصفية ذاتيًا لكن مع قيم أصغر، فيما تمثّل الحالة الخاصة ‎factorial <‎0‎‎>‎ شرطَ الإنهاء. وتنطبق على البرمجة الوصفية للقوالب معظم قيود لغات البرمجة الدالّية (functional programming language)، لذا فإنّ الذاتية (recursion) هي البنية الأساسية للتكرار "looping". ولمّا كانت دوال القالب الوصفية تُنفَّذ في وقت التصريف، فيمكن استخدام نتائجها في السياقات التي تتطلب قِيَمَ وقت التصريف (compiletime values). انظر المثال التالي: int my_array[factorial<5>::value]; يجب أن يُحدَّ حجم المصفوفات أثناء التصريف، وتكونَ نتيجة الدالة الوصفية قيمة ثابتة وقت التصريف كي يمكن استخدامها هنا. ملاحظة: لن تسمح معظم المُصرّفات بأن تتعمّق العوديّة أبعد من حدّ معيّن. على سبيل المثال، المُصرّف ‎g++‎ افتراضيًّا يحدُّ العوديّة في 256 مستوى. في حالة المصرّف ‎g++‎، يمكن للمُبرمج ضبط عمق الذاتية باستخدام الخيار ‎-ftemplate-depth-‎‎X‎. الإصدار ≥ C++‎ 11 منذ الإصدار C++‎ 11، يمكن استخدام قالب ‎std::integral_constant‎ في هذا النوع من حسابات القوالب: #include <iostream> #include <type_traits> template < long long n > struct factorial: std::integral_constant < long long, n * factorial < n - 1 > ::value > {}; template < > struct factorial < 0 >: std::integral_constant < long long, 1 > {}; int main() { std::cout << factorial < 7 > ::value << std::endl; // "5040" } كذلك فإن دوال ‎constexpr‎ بديل أفضل. انظر: #include <iostream> constexpr long long factorial(long long n) { return (n == 0) ? 1 : n * factorial(n - 1); } int main() { char test[factorial(3)]; std::cout << factorial(7) << '\n'; } يُكتب متن الدالة ‎factorial()‎ كتعليمة واحدة لأن دوال التعابير الثابتة constexpr في C++‎ 11 تستخدم قسمًا محدودًا للغاية من اللغة. الإصدار ≥ C++‎ 14 صار من السهل كتابة دوال التعابير الثابتة constexpr منذ إصدار C++ 14 بعد إسقاط العديد من القيود على كتابتها، انظر: constexpr long long factorial(long long n) { if (n == 0) return 1; else return n * factorial(n - 1); } أو حتى بالصورة التالية: constexpr long long factorial(int n) { long long result = 1; for (int i = 1; i <= n; ++i) { result *= i; } return result; } الإصدار ≥ C++‎ 17 بدءًا من الإصدار C++‎ 17، صار من الممكن استخدام تعبير مطوي (fold expression) لحساب المضروب، انظر: #include <iostream> #include <utility> template < class T, T N, class I = std::make_integer_sequence < T, N >> struct factorial; template < class T, T N, T...Is > struct factorial < T, N, std::index_sequence < T, Is... >> { static constexpr T value = (static_cast < T > (1) * ... * (Is + 1)); }; int main() { std::cout << factorial < int, 5 > ::value << std::endl; } التكرار على حزمة معامِلات قد نحتاج أحيانًا إلى إجراء عملية على كل عنصر من عناصر حزمة معاملات قالب متغير (variadic template parameter pack)، وهناك عدة طرق لفعل ذلك كما أنها صارت أسهل في C++‎ 17. إذا افترضنا أننا نريد طباعة كل عنصر في الحزمة يكون أبسط حل لدينا هو الذاتية (Recursion)، انظر المثال التالي: الإصدار C++‎ 11 void print_all(std::ostream & os) { // الحالة الأساسية } template < class T, class...Ts > void print_all(std::ostream & os, T const & first, Ts const & ...rest) { os << first; print_all(os, rest...); } نستطيع استخدام أسلوب الموسِّع expander بدلًا مما سبق من أجل تنفيذ كل عمليات البث (streaming) في دالة واحدة، ولم نكن لنحتاج إلى تحميلٍ زائدٍ آخر، لكن يعيب هذا الأسلوب أنه يجعل من الصعب قراءة الشيفرة. انظر: الإصدار C++‎ 11 template < class...Ts > void print_all(std::ostream & os, Ts const & ...args) { using expander = int[]; (void) expander { 0, (void(os << args), 0)... }; } راجع إجابة TC في موقع stackoverflow لتفهم كيفية عمل ما سبق بشكل أوضح. الإصدار ≥ C++‎ 17 لدينا بدءًا من الإصدار C++‎ 17 أَداتين جديدتين لحلّ هذه المشكلة، الأولى هي التعبير المطوي (fold-expression): template < class...Ts > void print_all(std::ostream & os, Ts const & ...args) { ((os << args), ...); } والثانية هي تعليمة ‎if constexpr‎، والتي تسمح لنا بكتابة حلّنا الذاتي الأول في دالة واحدة، انظر المثال التالي حيث يُستنسَخ سطر if constexpr في حالة وجود وسائط أخرى فقط، أما إن كان rest فارغًا فلن يكون أي استدعاءٍ لـ (print_all(os. template < class T, class...Ts > void print_all(std::ostream & os, T const & first, Ts const & ...rest) { os << first; if constexpr(sizeof...(rest) > 0) { print_all(os, rest...); } } التكرار عبر std::integer_sequence صار قالب الصنف (class template) متاحًا منذ إصدار C++‎ 14، انظر: template < class T, T...Ints > class integer_sequence; template < std::size_t...Ints > using index_sequence = std::integer_sequence < std::size_t, Ints... > ; وكذلك دالةُ عُليا مولِّدة (generating metafunction) لأجله: template < class T, T N > using make_integer_sequence = std::integer_sequence < T, /* a sequence 0, 1, 2, ..., N-1 */ > ; template < std::size_t N > using make_index_sequence = make_integer_sequence < std::size_t, N > ; ورغم أنّ هذا لم يصبح معتمدًا إلا منذ الإصدار C++‎ 14، إلا أنه يمكن تنفيذه باستخدام أدوات C++‎ 11، ونستطيع استخدام هذه الأداة لاستدعاء دالّة مع تمرير صفّ ‎std::tuple‎ من الوسائط (اعتُمِدَت في C++‎ 17 كـ ‎std::apply‎) انظر: namespace detail { template <class F, class Tuple, std::size_t... Is> decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) { return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...); } } template <class F, class Tuple> decltype(auto) apply(F&& f, Tuple&& tpl) { return detail::apply_impl(std::forward<F>(f), std::forward<Tuple>(tpl), std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{}); } // 3 هذا سيطبع int f(int, char, double); auto some_args = std::make_tuple(42, 'x', 3.14); int r = apply(f, some_args); // تستدعي f(42, 'x', 3.14) إرسال الوسوم (Tag Dispatching) إحدى أبسط الطرق للاختيار بين الدوال عند وقت التصريف هي إرسال دالة إلى زوج من الدوال المُحمّلة بحمل زائد والتي تأخذ وسمًا كأحد وسائطها (attributes) -يكون الأخير عادة-، فمثلًا لتطبيق ‎std::advance()‎، نستطيع تنفيذ الإرسال على فئة المُكرّر، انظر: namespace details { template < class RAIter, class Distance > void advance(RAIter & it, Distance n, std::random_access_iterator_tag) { it += n; } template < class BidirIter, class Distance > void advance(BidirIter & it, Distance n, std::bidirectional_iterator_tag) { if (n > 0) { while (n--) ++it; } else { while (n++) --it; } } template < class InputIter, class Distance > void advance(InputIter & it, Distance n, std::input_iterator_tag) { while (n--) { ++it; } } } template < class Iter, class Distance > void advance(Iter & it, Distance n) { details::advance(it, n, typename std::iterator_traits < Iter > ::iterator_category {}); } وسائط ‎std::XY_iterator_tag‎ الخاصّة بدوال ‎details::advance‎ ذات الأحمال الزائدة هي مُعامِلات غير مستخدمة، ذلك أن التطبيق الفعلي لا يهم لأنه فارغ تمامًا، والغرض الوحيد منها هو السماح للمصرّف باختيار التحميل الزائد بناءً على وسم الصنف ‎details::advance‎ الذي استُدعي معه. تستخدم ‎advance‎ في هذا المثال دالةَ ‎iterator_traits<T>::iterator_category‎ الوصفية، والتي تعيد أحد أصناف ‎iterator_tag‎ بناءً على نوع ‎Iter‎ الفعلي، ثم بعد ذلك يتيح كائن منشأ افتراضيًا من نوع iterator_category<Iter>::type للمصرِّف اختيار أحد تحميلات details::advance الزائدة. سيتجاوز المصرِّفُ على الأرجح معامِل الدالة ذاك بما أنه كائن منشأ افتراضيًا لِبُنيَة فارغة لم تستخدم. إرسال الوسوم يجعل الشيفرة أسهل في القراءة مقارنة بغيرها، وذلك باستخدام قاعدة "خطأ التعويض ليس خطأً" أو SFINAE اختصارًا بالإنجليزية، وكذلك استخدام enable_if. ملاحظة: رغم أنّ if constexpr قد تبسِّط تنفيذ advance بشكل خاص، فإنها -خلاف إرسال الوسوم- غير مناسبة للتطبيقات المفتوحة (open implementations). التحقق من صحة تعبير ما نستطيع التحقق إن كان يجوز استدعاء عامل (operator) أو دالة على نوع ما، كذلك يمكن التحقق إن كان صنف ما لديه تحميل زائد‏‏ على std::hash، وذلك كما يلي: #include <functional> // for std::hash #include <type_traits> // for std::false_type and std::true_type #include <utility> // for std::declval template<class, class = void> struct has_hash : std::false_type {}; template<class T> struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())> : std::true_type {}; الإصدار ≥ C++‎ 17 يمكن استخدام ‎std::void_t‎ لتبسيط هذا النوع من الإنشاءات منذ الإصدار C++‎ 17. #include <functional> // for std::hash #include <type_traits> // for std::false_type, std::true_type, std::void_t #include <utility> // for std::declval template<class, class = std::void_t<> > struct has_hash : std::false_type {}; template<class T> struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > > : std::true_type {}; حيث تٌعرَّف ‎std::void_t‎ على النحو التالي: template< class... > using void_t = void; للتحقق مما إذا كان أحد العوامل مثل ‎operator<‎ مُعرّفًا، فإن بنية الشيفرة تكون نفسها تقريبًا، انظر: template<class, class = void> struct has_less_than : std::false_type {}; template<class T> struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())> : std::true_type {}; يمكن الاستفادة مما سبق واستخدامه عند استخدام ‎std::unordered_map<T>‎ إن كان لـ ‎T‎ تحميل زائد على ‎std::hash‎، لكن فيما سوى ذلك فاستخدام ‎std::map<T>‎: template <class K, class V> using hash_invariant_map = std::conditional_t< has_hash<K>::value, std::unordered_map<K, V>, std::map<K,V>>; If-then-else الإصدار ≥ C++‎ 11 يستطيع نوعُ ‎std::conditional‎ الموجود في ترويسة المكتبة القياسية اختيارَ نوع ما استنادًا إلى قيمة بوليانية محدّدة وقتَ التصريف: template < typename T > struct ValueOrPointer { typename std::conditional < (sizeof(T) > sizeof(void * )), T * , T > ::type vop; }; تحتوي هذه البنية مؤشّرًا إلى ‎T‎ إن كان ‎T‎ أكبر من حجم المؤشّر، أو إلى ‎T‎ نفسه إذا كان أصغر أو يساوي حجم المؤشر، وعليه سيكون ‎sizeof(ValueOrPointer)‎ أصغر دائمًا من ‎sizeof(void*)‎. التمييز اليدوي للأنواع عند إعطائها أيَّ نوع T عند تطبيق قاعدة SFINAE باستخدام ‎std::enable_if‎ يكون مفيدًا أن نصل إلى قوالب مساعِدةٍ (helper templates) تحدّدُ إذا كان نوع ‎T‎ المُعطى يطابق معايير معيّنة. وتوفّر المكتبة القياسيّة نوعين مشابهين لـ ‎true‎ و ‎false‎، وهما ‎std::true_type‎ و ‎std::false_type‎، واللذان يمكن استخدامُهما لتحقيق الغرض أعلاه. يوضّح المثال التالي كيفية التحقّق ممّا إذا كان نوع ‎T‎ مُؤشِّرًا (pointer) أم لا، ويحاكي قالبُ ‎is_pointer‎ سلوكَ دالة ‎std::is_pointer‎ المساعِدة القياسية: template < typename T > struct is_pointer_: std::false_type {}; template < typename T > struct is_pointer_ < T * >: std::true_type {}; template < typename T > struct is_pointer: is_pointer_ < typename std::remove_cv < T > ::type > {} تتألّف الشيفرة أعلاه من ثلاث خطوات (قد لا نحتاج أكثر من خطوتين أحيانًا): تصريح ‎is_pointer_‎ الأوّل هو الحالة الافتراضية، ويرث من ‎std::false_type‎. يجب أن ترث الحالة الافتراضيّة دائمًا من ‎std::false_type‎ لأنّها تشبه "الشرط الخاطئ ‎false‎". التصريح الثاني يخصّص القالب ‎is_pointer_‎ لأجل المؤشّر ‎T*‎ بغضِّ النظر عن ماهية ‎T‎، وترث هذه النسخة من ‎std::true_type‎. التصريح الثالث (وهو التصريح الحقيقي) يزيل أيّ معلومات غير ضروريّة من ‎T‎ (في هذه الحالة يزيل المؤهّليْن (qualifiers) ‏‏‎const‎ و ‎volatile‎) ثم يعود إلى أحد التصريحين السابقين. وبما أنّ ‎is_pointer<T>‎ هو صنفٌ فإننا نحتاج إلى التالي من أجل الوصول إلى قيمته: استخدم ‎::value‎، مثلًا: ‎is_pointer<int>::value‎: القيمة ‎value‎ هو عضو صنفٍ ثابت (static class member) من النوع ‎bool‎ موروثٌ من std::true_type أو std::false_type. أنشئ كائنًا من هذا النوع، على سبيل المثال ‎is_pointer<int>{}‎: ذلك مناسب لأنّ ‎std::is_pointer‎ ترث منشئها الافتراضي من ‎std::true_type‎ أو ‎std::false_type‎ (التي لها مُنشئات من نوع ‎constexpr‎)، ولكلٍّ من std::true_type و std::false_type معاملاتُ تحويل من constexpr إلى bool. من الجيد توفير مساعِدات للقوالب المساعدة، تتيح الوصول المباشر إلى القيمة، انظر: template <typename T> constexpr bool is_pointer_v = is_pointer<T>::value; الإصدار ≥ C++‎ 17 توفر معظم قوالب المساعدة في إصدار C++ 17 وما بعده نسخةَ ‎_v‎، انظر المثال التالي: template< class T > constexpr bool is_pointer_v = is_pointer<T>::value; template< class T > constexpr bool is_reference_v = is_reference<T>::value; حساب القوة في C++‎ 11 وما بعدها صارت العمليات الحسابية في وقت التصريف أيسر بكثير منذ إصدار C++‎ 11، فيمكن حساب قوة عدد معين في وقت التصريف مثلًا على النحو التالي: template < typename T > constexpr T calculatePower(T value, unsigned power) { return power == 0 ? 1 : value * calculatePower(value, power - 1); } وتكون الكلمة المفتاحية ‎constexpr‎ مسؤولة عن حساب الدالة في وقت التصريف عند استيفاء جميع المتطلّبات اللازمة، كأن تكون جميع الوسائط معروفة في وقت التصريف. ملاحظة: يجب أن تكون دوال التعبير الثابت (‎constexpr‎) في C++‎ 11 مكونة من تعليمة return واحدة فقط. عند مقارنة هذه الطريقة بالطريقة القياسية لحسابات وقت التصريف، فإن هذه الطريقة مفيدة أيضًا في حسابات وقت التشغيل، فإن كانت وسائط دالة ما مجهولةً وقتَ التصريف (مثل القيمة والقوة المعطاتيْن كمدخلات من قِبل المستخدم) فإن الدالة تُنفَّذ في وقت التصريف، فلا حاجة إذًا لتكرار الشيفرة كما كان في المعايير الأقدم من C++‎. انظر المثال التالي: void useExample() { constexpr int compileTimeCalculated = calculatePower(3, 3); // الحساب وقت التصريف // لأنّ كلا المُعاملين معروفان وقت التصريف ويُستخدم في تعبير ثابت int value; std::cin >> value; int runtimeCalculated = calculatePower(value, 3); // runtime calculated, // لأنّ القيمة لن تكون معروفة حتى وقت التشغيل } الإصدار ≥ C++‎ 17 وهناك طريقة أخرى لحساب القوة في وقت التصريف تستخدم التعابير المطوية، انظر: #include <iostream> #include <utility> template <class T, T V, T N, class I = std::make_integer_sequence<T, N>> struct power; template <class T, T V, T N, T... Is> struct power<T, V, N, std::integer_sequence<T, Is...>> { static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1))); }; int main() { std::cout << power < int, 4, 2 > ::value << std::endl; } دالة عامّة لتحديد القيم الصغرى والعظمى مع عدد وسائط متغير الإصدار> C++‎ 11 من الممكن كتابة دالة عامة (على سبيل المثال دالة تحسب الحد الأدنى ‎min‎) تقبل أنواعًا عدديّة مختلفة، وعدد وسائط عشوائي بواسطة قالَب برمجة وصفية. انظر المثال التالي حيث تصرّح الدالة عن دالة ‎min‎ تقبل وسيطين أو أكثر. template <typename T1, typename T2> auto min(const T1 &a, const T2 &b) -> typename std::common_type<const T1&, const T2&>::type { return a < b ? a : b; } template <typename T1, typename T2, typename ... Args> auto min(const T1 &a, const T2 &b, const Args& ... args) -> typename std::common_type<const T1&, const T2&, const Args& ...>::type { return min(min(a, b), args...); } auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9); حسابيات البرمجة الوصفية فيما يلي أمثلة على استخدام برمجة القوالب الوصفية في C++‎ لإنجاز العمليات الحسابية في وقت التصريف. حساب الأس ‎في (O(log n يوضّح هذا المثال طريقة فعالة لحساب الأسّ باستخدام برمجة القوالب الوصفية. template <int base, unsigned int exponent> struct power { static const int halfvalue = power<base, exponent / 2>::value; static const int value = halfvalue * halfvalue * power<base, exponent % 2>::value; }; template <int base> struct power<base, 0> { static const int value = 1; static_assert(base != 0, "power<0, 0> is not allowed"); }; template <int base> struct power<base, 1> { static const int value = base; }; مثال تطبيقي: std::cout << power < 2, 9 > ::value; الإصدار ≥ C++‎ 14 هذا المثال يعالج الأسّ السلبيّ أيضًا: template <int base, int exponent> struct powerDouble { static const int exponentAbs = exponent < 0 ? (-exponent) : exponent; static const int halfvalue = powerDouble<base, exponentAbs / 2>::intermediateValue; static const int intermediateValue = halfvalue * halfvalue * powerDouble<base, exponentAbs %2>::intermediateValue; constexpr static double value = exponent < 0 ? (1.0 / intermediateValue) : intermediateValue; }; template <int base> struct powerDouble<base, 0> { static const int intermediateValue = 1; constexpr static double value = 1; static_assert(base != 0, "powerDouble<0, 0> is not allowed"); }; template <int base> struct powerDouble<base, 1> { static const int intermediateValue = base; constexpr static double value = base; }; int main() { std::cout << powerDouble<2,-3>::value; } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 16: Metaprogramming والفصل Chapter 125: Arithmitic Metaprogramming من كتاب C++ Notes for Professionals
  6. case تُدخِل كلمة case المفتاحية وسم حالة (case label) لتعليمة switch، وتنفّذ تعليمات معيّنة بناء على القيمة التي يساويها معامَلها. كذلك يجب أن يكون هذا المعامَل تعبيرًا ثابتًا ومطابقًا لشرط تعليمة switch. تنتقل تعليمة switch عند تنفيذها إلى وسم الحالة التي يساوي فيها المعامَلُ الشرطَ إن وُجِد. انظر المثال التالي: char c = getchar(); bool confirmed; switch (c) { case 'y': confirmed = true; break; case 'n': confirmed = false; break; default: std::cout << "invalid response!\n"; abort(); } switch وفقًا لتوصيف C++‎، فإن تعليمة switch تحول التحكم إلى تعليمة برمجية او أكثر وفقًا لقيمة الشرط، وتُتبَع كلمة ‎switch‎ المفتاحية بقوسين يحتويان شرطًا ثم كتلة برمجية، والتي قد تحتوي على عناوين ‎case‎، وعنوان ‎default‎ اختياري. عند تنفيذ تعليمة switch سيُنقَل التحكّم إلى عنوان ‎case‎ ذي القيمة المطابقة لقيمة الشّرط -إن وُجدت-، أو إلى العنوان ‎default‎ إن وُجد أيضًا. ويجب أن يكون الشرط تعبيرًا أو تصريحًا يحتوي على عدد صحيح، أو نوع عددي (enumeration type)، أو نوع صنف له دالة تحويل إلى عدد صحيح أو نوع عددي. char c = getchar(); bool confirmed; switch (c) { case 'y': confirmed = true; break; case 'n': confirmed = false; break; default: std::cout << "invalid response!\n"; abort(); } catch تُدخِل كلمة ‎catch‎ المفتاحية معالج اعتراضات ينتقل التحكّم إلى كتلته عند إطلاق اعتراض (exception) من النّوع الموافق. وتُعقَب ‎catch‎ بقوسين يحتويان تصريحًا عن اعتراض، وذلك يشبه في هيئته تصريحات المُعامِلات في الدّوال، إذ يجوز حذف اسم المعامِل ويُسمح بالمقطع ‎...‎، والذي يطابق أي نوع. مُعالجُ الاعتراض لن يُعالجَ الاعتراض إلّا إن كان تصريحه موافقًا لنوع الاعتراض. لمزيد من التفاصيل، انظر إمساك الاعتراضات. try { std::vector < int > v(N); // افعل شيئا ما } catch (const std::bad_alloc & ) { std::cout << "failed to allocate memory for vector!" << std::endl; } catch (const std::runtime_error & e) { std::cout << "runtime error: " << e.what() << std::endl; } catch (...) { std::cout << "unexpected exception!" << std::endl; throw; } throw عندما تقع ‎throw‎ في تعبير ذي معامَل فإنّها تطلق اعتراضًا يكون نسخة من ذلك المعامَل: void print_asterisks(int count) { if (count < 0) { throw std::invalid_argument("count cannot be negative!"); } while (count--) { putchar('*'); } } عندما تقع ‎throw‎ في تعبير ليس له معامَل، فستعيد إطلاق الاعتراض الحالي، أما إن لم يكن ثمة اعتراض موجود فتُستدعى ‎std::terminate‎. try { // افعل شيئا ما } catch (const std::bad_alloc & ) { std::cerr << "out of memory" << std::endl; } catch (...) { std::cerr << "unexpected exception" << std::endl; // رجاء أن يعرف المستدعي كيفية معالجة هذا الاعتراض. throw; } عندما تقع ‎throw‎ في مُصرِّح دالة (function declarator)، فستقدّم توصيفًا للاعتراضات الديناميكية، والذي يسرد أنواع الاعتراضا التي يجوز للدّالة نشرها. انظر المثال التالي لدالة قد تنشر الاعتراض std::runtime_error مثلًا، لكن ليس std::logic_error: void risky() throw(std::runtime_error); // هذه الدالة لا يمكنها نشر أيّ اعتراض. void safe() throw(); أُهملت توصيفات الاعتراض الديناميكية بدءًا من C++ 11. أول استخدامَين للعبارة ‎throw‎ المذكورة أعلاه هما تعبيران (expressions) وليسا تعليمتيْن (statements)، لاحظ أن نوع الاعتراض الذي أُطلِق هو ‎void‎، هذا يجعل تداخلها ممكنًا في التعبيرات على النحو التالي: unsigned int predecessor(unsigned int x) { return (x > 0) ? (x - 1) : (throw std::invalid_argument("0 has no predecessor")); } default تمثل default العنوان الذي سيقفز إليه البرنامج في تعليمة switch، إذا كانت قيمة الشرط لا تساوي أيًا من قيم عناوين case. char c = getchar(); bool confirmed; switch (c) { case 'y': confirmed = true; break; case 'n': confirmed = false; break; default: std::cout << "invalid response!\n"; abort(); } الإصدار ≥ C++11 تُعرّف default مُنشِئًا افتراضيًا (default constructor) أو مُنشئ نسخ أو نقل، أو مُدمِّرًا (destructor)، أو عامل إسناد النّسخ (copy assignment operator)، أو عامل إسناد النقل (move assignment operator) ليكون هو السلوك الافتراضي. انظر المثال التالي حيث نريد أن نكون قادرين على محو الأصناف المشتقة عبر *Base، لكن نريد السلوك المعتاد لمدمر Base: class Base { virtual~Base() = default; }; try تُتبَع الكلمة المفتاحية ‎try‎ بكتلة برمجية، أو قائمة تهيئة المُنشئ (constructor initializer list) ثمّ كتلة برمجية. وتُتبَع كتلة try بكتلة catch واحدة أو أكثر، وإذا انتشر اعتراض خارج كتلة try فإنّ كل كتل تعليمات catch الموجودة بعد كتلة try تستطيع معالجة الاعتراض إذا كانت الأنواع متطابقة. انظر المثال التالي حيث لن تمسك catch الاعتراض إن أُطلق بعد std::vector الأولى، لكنها ستمسكه إن أُطلق بعد الثانية التي داخل try: std::vector < int > v(N); try { std::vector < int > v(N); // v افعل شيئا ما بـ } catch (const std::bad_alloc & ) { // try من كتلة bad_alloc عالج اعتراض. } if if هي تعليمة شَرْطية يجب أن تُتبَع بقوسين يضُمّان شرطًا يكون إمّا تعبيرًا أو تصريحًا، وإذا تحقّق ذلك الشّرط، فستُنفّذ العبارة الموجودة بعده. انظر: int x; std::cout << "Please enter a positive number." << std::endl; std::cin >> x; if (x <= 0) { std::cout << "You didn't enter a positive number!" << std::endl; abort(); } else قد تَتبع كلمة else المفتاحيةُ أولَ تعليمة فرعية لتعليمة if، وستُنفَّذ التعليمات الفرعية بعد ‎else‎ عند عدم تحقق شرط if، أي عندما لا تُنفّذ كتلة التعليمات الأولى. انظر: int x; std::cin >> x; if (x % 2 == 0) { std::cout << "The number is even\n"; } else { std::cout << "The number is odd\n"; } البُنى الشرطية: if و if..else if و else تُستخدم لنعرف ما إن كان التعبير المُعطى يعيد true أو false ومن ثم تتصرف وفقها، انظر: if (condition) statement يمكن أن يكون الشّرط أيّ تعبير C++‎ صالح يُعيد شيئًا يمكن التحقق من صحته أو خطئه، انظر المثال التالي حيث تُنفَّذ الشيفرة بين القوسين لتحقق الشرط (أي true)، بينما لا تُنفَّذ الشيفرة في السطر الثاني لعدم تحققه (أي false): if (true) { /* الشيفرة المراد تنفيذها */ } if (false) { /* الشيفرة المراد تنفيذها */ } قد يكون الشرط دالة أو متغيرًا أو مقارنة أو غير ذلك، انظر المثال التالي: // تنفَّذ الشيفرة إن تحققت الدالة if(istrue()) { } // تقييم الدالة بعد تمرير المتغير إليها if(isTrue(var)) { } // a يساوي b ستُنفّذ الشيفرة المقابلة إن كان if(a == b) { } // قيمة بوليانية فستُقيّم بحسب قيمتها الفعلية a إن كانت // وإن كانت عددا، فإنّ أي قيمة غير صفرية فستُعد صحيحة if(a) { } تستطيع التحقق من عدة تعبيرات بإحدى طريقتين: استخدام العمليات الثنائية‎: if (a && b) { } // معًا a و b تتحقق في حال تحقق scope here if (a || b ) { } // تتحقق إن تحقق أي منهما. استخدام if / ifelse / else: لإجراء تبديل بسيط، يمكنك استخدام إمّا if أو else: if (a == "test") { // "test" تساوي a ستُنفّذ في حال كانت } else { // تنفذ في حال لم تنفّذ العبارة الأولى } وفي حالة الخيارات المتعددة: if (a == 'a') { // 'a' حرفا يساوي a إن كانت } else if (a == 'b') { // 'b' حرفا يساوي a إن كانت } else if (a == 'c') { // 'c' حرفا يساوي a إن كانت } else { // تُنفّذ إن لم يتحقق أي مما سبق } يُفضّل استخدام "switch " بدلاً من الشيفرة السابقة ما دُمنا نتحقّق من قيمة نفس المتغير. goto تنتقل goto إلى التعليمة المعنونة (labelled statement)، والتي ينبغي أن تكون موجودة في الدالة الحاليّة. bool f(int arg) { bool result = false; hWidget widget = get_widget(arg); if (!g()) { // لا يمكن أن نستمر، لكن لا زال علينا أن ننظف. goto end; } // ... result = true; end: release_widget(widget); return result; } عبارات القفز: break و continue و goto و exit التعليمة break نستطيع مغادرة الحلقة التكرارية باستخدام break -انظر الحلقات التكرارية- حتى لو لم يتحقق شرط إنهائها، كما يمكن استخدامها مثلًا لإنهاء حلقة لا نهائية أو لإجبارها على التوقف قبل نهايتها الطبيعية. وتكون صيغتها كالتالي: break; على سبيل المثال: غالبًا ما نستخدم ‎break‎ في حالات ‎switch‎، كأن تتحقق حالة في switch فتُنفَّذ شيفرة الشرط. انظر: switch (conditon) { case 1: block1; case 2: block2; case 3: block3; default: blockdefault; } في الشيفرة السابقة، إن تحققت الحالة الأولى case 1 فإن الكتلة block 1 تُنفَّذ، ثم تُنفَّذ كتل الحالتين الثانية والثالثة رغم أن الحالة الأولى فقط هي المتحققة، وهي ما أردنا تنفيذ الشيفرة المقابلة لها فقط. ولتجنب تنفيذ الشيفرات للحالات التي لم تحقق فإننا نستخدم break في نهاية كل كتلة، انظر: switch (condition) { case 1: block1; break; case 2: block2; break; case 3: block3; break; default: blockdefault; break; } لن تُعالَج الآن إلا كتلة واحدة فقط، وسينتقل التحكم إلى خارج حلقة switch، كذلك يمكن استخدام break في الحلقات الشرطية وغير الشرطية الأخرى، مثل ‎if‎ و ‎while‎ و ‎for‎ وغيرها؛ انظر: if (condition1) { .... if(condition2) { ....... break; } ... } التعليمة continue تجعل التعليمة continue البرنامجَ يتخطّى بقيّة تعليمات الحلقة في التكرار الحالي ممّا يؤدّي إلى الانتقال إلى التكرار التالي في الحلقة، وتكون صيغتها كالتالي: continue; انظر المثال التالي: for (int i = 0; i < 10; i++) { if (i % 2 == 0) continue; cout << "\n @" << i; } الذي ينتج الخرج: @1 @3 @5 @7 @9 في المثال السابق، كلمّا تحقّق الشرط ‎i%2==‎0‎ فستُنفّذ العبارة ‎continue‎، مما يجعل المُصرّف يتخطّى كل الشيفرة المتبقيّة (طباعة @ و i)، ويعودَ لتنفيذ عبارة الزّيادة/الإنقاص (increment/decrement) في الحلقة. التعليمة goto تسمح goto بإجراء قفزة إلى نقطة أخرى في البرنامج. يجب عليك الحذر عند استخدام هذه الميزة لأنّ تنفيذها يتجاهل كلّ قيود التداخل (nesting). وتُحدَّد النقطة التي سينتقل إليها البرنامج بواسطة عنوان (label) يُمرَّر بعدها كوسيط لتعليمة goto، ويتألّف اسم ذلك العنوان من مُعرِّف صالح متبوع بنقطتين : على النحو التالي: goto label; .. . label: statement; ملاحظة: يُوصى بتجنّب استخدام عبارة goto لأنها تصعّب تتبّع سير البرنامج ومن ثم فهمه وتعديله. مثال: int num = 1; STEP: do { if (num % 2 == 0) { num = num + 1; goto STEP; } cout << "value of num : " << num << endl; num = num + 1; } while (num < 10); الناتج: pre widget value of num : 1 value of num : 3 value of num : 5 value of num : 7 value of num : 9 تنقل goto تنفيذَ التحكم، عند تحقق شرط ‎num%2==‎0‎، إلى بداية حلقة do-while دالة exit ‎exit‎ هي دالة مُعرَّفة في مكتبة cstdlib، والغرض منها هو إنهاء البرنامج الذي يتم تنفيذه برمز خروج محدّد يكون على الصورة التالية: void exit (int exit code); رمزا الخروج القياسيَّين EXIT_SUCCESS و EXIT_FAILURE مُعرَّفان في مكتبة ‎cstdlib‎. return تعيد تعليمة return التحكمَ من الدالة إلى مُستدعيها، وإذا كان للتعليمة مُعامَل فسيُحوَّل إلى نوع القيمة المعادة الخاص بالدالة، ثم تُعاد القيمة المُحوّلة إلى المستدعي. int f() { return 42; } int x = f(); // x = 42 int g() { return 3.14; } int y = g(); // x = 3 إذا لم يكن للتعليمة ‎return‎ أيّ معامَل فينبغي أن يكون نوع القيمة المُعادة للدالة هو ‎void‎. كذلك يمكن للدوال التي نوع قيمتها المعادة فارغ (‎void‎) أن تعيد تعبيرًا إن كان ذلك التعبير من النوع ‎void‎. void f(int x) { if (x < 0) return; std::cout << sqrt(x); } int g() { return 42; } void h() { return f(); // ثم العودة f استدعاء return g(); // غير صالحة } تستدعي main دالة ‎std::exit‎ ضمنيًا مع القيمة التي تعيدها، وتعاد القيمة عندها إلى بيئة التنفيذ، غير أن الإعادة من main يدمر المتغيرات المحلية التلقائية في حين أن استدعاء ‎std::exit‎ مباشرة لا يفعل ذلك. int main(int argc, char ** argv) { if (argc < 2) { std::cout << "Missing argument\n"; return EXIT_FAILURE; // exit(EXIT_FAILURE); يكافئ } } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 15: Flow Control من كتاب C++ Notes for Professionals
  7. تستخدم لغة C++‎ مجاري التدفق لإدارة دخل وخرج الملفات، إذ تستخدم: std::istream لقراءة النص. std::ostream لكتابة النص. std::streambuf لقراءة أو كتابة المحارف. يستخدم الدَّخل المُنسّق (Formatted input) العاملَ‏‏ ‎operator>>‎. يستخدم الخرج المنسّق العامل ‎operator<<‎. تستخدم المجاري ‎std::locale‎ مثلًا للحصول على تفاصيل التنسيق والترجمة بين الترميزات الخارجية والترميز الداخلي. الكتابة في الملفات هناك عدة طرق للكتابة في ملف، لعل أسهلها هو استخدام مجرى خرج الملفّات ‎ofstream‎ مع عامل الإدارج في المجرى (‎<<‎)، انظر: std::ofstream os("foo.txt"); if(os.is_open()){ os << "Hello World!"; } تستطيع استخدام الدالة التابعة لمجرى خرج الملفّات ‎write()‎ بدلًا من ‎<<‎، انظر المثال التالي حيث سنكتب ثلاثة محارف من data: std::ofstream os("foo.txt"); if (os.is_open()) { char data[] = "Foo"; os.write(data, 3); } يجب أن تتحقق دائمًا بعد الكتابة في المجرى من راية حالة الخطأ badbit، إذ أنها تشير إلى نجاح العملية أو فشلها، وتستطيع ذلك باستدعاء دالة تابع مجرى خَرْج الملف ‎bad()‎، انظر: os << "Hello Badbit!"; // قد تفشل هذه العملية لسبب ما if (os.bad()) // فشل في الكتابة فتح ملف تُفتح الملفات بنفس الطّريقة في جميع مجاري الملفات الثلاث (‎ifstream‎، ‎ofstream‎، و‎fstream‎). يمكنك فتح الملف مباشرة في المُنشئ (constructor)، انظر المثال التالي حيث نفتح الملف foo.txt للقراءة فقط، ثم للكتابة فقط، ثم للقراءة والكتابة: std::ifstream ifs("foo.txt"); std::ofstream ofs("foo.txt"); std::fstream iofs("foo.txt"); أو يمكنك استخدام دالة تابع مجرى الملف ‎open()‎ لتنفيذ نفس الشيء: std::ifstream ifs; ifs.open("bar.txt"); // للقراءة فقط "bar.txt" فتح الملف std::ofstream ofs; ofs.open("bar.txt"); // للكتابة فقط "bar.txt" فتح الملف std::fstream iofs; iofs.open("bar.txt"); // للقراءة والكتابة "bar.txt" فتح الملف يجب عليك التحقّق دائمًا ممّا إذا كان الملف قد فُتِح بنجاح (حتى أثناء الكتابة)، قد تفشل عملية الفتح لعدة أسباب، انظر بعضها فيما يلي: عدم وجود الملف صلاحيات الوصول (access rights) غير صالحة. الملف قيد الاستخدام في الوقت الراهن. وقوع خطأ في القرص. فصل القرص من الحاسوب. … يمكن إجراء عملية التحقّق على النحو التالي: // 'foo.txt' محاولة قراءة std::ifstream ifs("fooo.txt"); // خطأ في الكتابة، لا يمكن فتح الملف // التحقق مما إذا كان الملف قد فُتِح بنجاح if (!ifs.is_open()) { // لم يُفتح الملف بعد، اتخاذ الإجراءات المناسبة throw CustomException(ifs, "الملف لم يُفتح"); } إن كان المسار يحتوي على شرطة مائلة عكسية \ كما هو الحال في نظام Windows، فيجب أن تهربها بشكل سليم. انظر المثال التالي لفتح الملف foo.txt في ويندوز: std::ifstream ifs("c:\\\\folder\\\\foo.txt"); // تهريب الشرطة المائلة العكسية الإصدار ≥ C++‎ 11 أو يمكنك استخدام القيم مصنفة النوع الخام (raw literal)، انظر: std::ifstream ifs(R"(c:\\folder\\foo.txt)"); // استخدام قيم مصنفة النوع خام أو استخدم شرطة مائلة / بدلاً من ذلك: std::ifstream ifs("c:/folder/foo.txt"); الإصدار ≥ C++‎ 11 إن أردت أن تفتح ملفًّا يحتوي على محارف من غير ASCII في مسارٍ في Windows، فيمكنك حاليًا استخدام المَحرَف العام غير القياسي (non-standard wide character) في المسار، انظر المثال التالي حيث نضع كلمة "مثال" بالبلغارية في مسار الملف: std::ifstream ifs(LR"(пример\\foo.txt)"); // استخدام محرف عام مع سلسلة نصية خام القراءة من ملف هناك عدة طرق لقراءة البيانات من ملف، فإن كنت تعرف تنسيق (format) البيانات فيمكنك استخدام عامل الاستخراج من المجرى - stream extraction operator‏‏ - (‎>>‎). دعنا نفرض أنّ لديك ملفّا يُسمّى foo.txt يحتوي على البيانات التالية: John Doe 25 4 6 1987 Jane Doe 15 5 24 1976 فعندها يمكنك استخدام الشّيفرة أدناه لقراءة تلك البيانات من الملف، لاحظ أننا سنستخرج الاسم الأول firstname واسم العائلة lastname والعمر age وشهر الولادة bmonth ويوم الولادة bday وعام الولادة byear. أيضًا، انتبه إلى أن ‎>>‎ تعيد القيمة false إن بلغت نهاية الملف أو لم تتوافق بيانات الدخل مع نوع المتغير، فلا يمكن استخراج نص foo إلى متغير int مثلًا. // تعريف المتغيرات std::ifstream is("foo.txt"); std::string firstname, lastname; int age, bmonth, bday, byear; while (is >> firstname >> lastname >> age >> bmonth >> bday >> byear) // معالجة البيانات المقروءة يستخرج العامل ‎>>‎ كل المحارف ويتوقف إذا وجد حرفًا لا يمكنه تخزينه أو محرفًا خاصًّا (special character): بالنسبة للسًلاسل النصية، يتوقًف العامل عند المسافة الفارغة ()، أو عند السًطر الجديد (‎\n‎). بالنًسبة للأعداد، يتوقف العامل عند المحارف غير الرقمية. هذا يعني أنّ النُّسخ التالية من ملف foo.txt ستُقرأ بنجاح من قبل الشّيفرة السابقة: John Doe 25 4 6 1987 Jane Doe 15 5 24 1976 يعيدُ عاملُ ‎>>‎ المجرى المُمرّرَ إليه، لهذا يمكن سَلْسَلةُ هذا العامل من أجل قراءة البيانات على التوالي. كذلك من الممكن استخدام المجرى كتعبير بولياني (كما هو مُوضّح في حلقة ‎while‎ في الشّيفرة السابقة)، ذلك أن أصناف المجرى بها عامل تحويل للنوع ‎bool‎. سيعيد العامل ‎bool()‎ القيمة ‎true‎ طالما أنّ المجرى لا يحتوي على أخطاء، أما إن تغيّرت حالة المجرى إلى حالة خطأ (على سبيل المثال، إذا لم يكن من الممكن استخراج مزيد من البيانات)، فسوف يعيد العاملُ ‎bool()‎ القيمة‎false‎، وعليه فإن حلقة ‎while‎ في الشّيفرة السابقة ستُنهى بعد قراءة كامل الملف. إذا أردت قراءة كامل الملف كسلسلة نصّية، فيمكنك استخدام الشّيفرة التالية: // 'foo.txt' فتح std::ifstream is("foo.txt"); std::string whole_file; // الذهاب إلى نهاية الملف is.seekg(0, std::ios::end); // تخصيص ذاكرة للملف whole_file.reserve(is.tellg()); // الذهاب إلى بداية الملف is.seekg(0, std::ios::beg); // 'whole_file' تعيين محتوى // إلى جميع محارف الملف whole_file.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>()); توفر هذه الشيفرة مساحة للسّلسلة النصية ‎string‎ من أجل الاقتصاد في الذاكرة. أما إن أردت قراءة الملف سطرًا سطرًا، فيمكنك استخدام الدالة ‎getline()‎، انظر المثال التالي حيث تعيد هذه الدالة القيمة false إن لم يكن ثمة أسطر أخرى متبقية: std::ifstream is("foo.txt"); for (std::string str; std::getline(is, str);) { // معالجة السّطر المقروء } إذا أردت قراءة عدد محدّد من المحارف فاستخدم دالة تابع المجرى ‎read()‎: std::ifstream is("foo.txt"); char str[4]; // قراءة أربعة حروف من الملف is.read(str, 4); يجب أن تتحقق دائمًا بعد تنفيذ أمر القراءة من راية حالة الخطأ failbit إذ أنها تشير إلى نجاح العملية أو فشلها، وتستطيع ذلك باستدعاء دالة تابع مجرى خَرْج الملف ‎fail()‎، انظر: is.read(str, 4); // قد تفشل هذه العملية لسبب ما if (is.fail()) // فشل في القراءة! أوضاع الفتح يمكنك تحديد وضع الفتح عند إنشاء مجرى ملف، ووضع الفتح ما هو إلا إعداد للتّحكم في كيفيّة فتح المجرى للملف، تستطيع العثور على جميع الأوضاع في مجال اسم ‎std::ios‎. يمكن تمرير وضع الفتح كمعامل ثاني إلى منشئ مجرى الملف أو إلى تابعه ‎open()‎: std::ofstream os("foo.txt", std::ios::out | std::ios::trunc); std::ifstream is; is.open("foo.txt", std::ios::in | std::ios::binary); تجدر الإشارة إلى أنّه يجب عليك تعيين معامِل الوضع الافتراضي ‎ios::in‎ أو ‎ios::out‎ إذا أردت تعيين قيم الرايات الأخرى، لأنها لا تٌعيّن ضمنيًا من قبل أعضاء مجرى الدّخل (iostream) رغم أنّ لديها قيمة افتراضية صحيحة. يُستخدم وضع الفتح الافتراضي أدناه إذا لم تحدِّد وضعًا للفتح: ifstream - دخل ofstream - خرج fstream - دخل وخرج أوضاع الفتح التي تستطيع تحديدها هي: الوضع المعنى الغرض الوصف app ألحِق (append) إخراج إضافة البيانات إلى نهاية الملف binary ثنائي/بِتِّي (Binary) إخراج/إدخال الإدخال والإخراج يحدث بالبتات in دخل (input) إدخال فتح الملف للقراءة out خرج (output) إخراج فتح الملف للكتابة trunc بتر (truncate) إخراج/إدخال حذف محتوى الملف عند الفتح ate عند النهاية (at end) إدخال الذهاب إلى نهاية الملف عند الفتح table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } ملاحظة: في حال تعيين الوضع البتّي ‎binary‎، فستُقرأ وتُكتب البيانات تمامًا كما هي؛ أما إن لم تُحدَّد فسيكون ممكنًا ترجمة محرف السّطر الجديد ‎'\n'‎ إلى نهاية السّطر المناسبة لنظام التشغيل المستخدم. على سبيل المثال، في نظام Windows، تسلسل نهاية السطر هو CRLF ‏‏(‎"\r\n"‎). الكتابة: "\n" => "\r\n" القراءة: "\r\n" => "\n" قراءة ملف ASCII إلى مكتبة std::string انظر المثال التالي، سيكون محتوى file.txt موجودًا في buffer.str()‎: std::ifstream f("file.txt"); if (f) { std::stringstream buffer; buffer << f.rdbuf(); f.close(); } يعيد التابع ‎rdbuf()‎ مؤشرًا إلى ‎streambuf‎، والذي يمكن إضافته إلى المخزن المؤقّت ‎buffer‎ عبر دالة التابع stringstream::operator<<‎. هناك احتمال آخر (من اقتراح سكوت مايرز): std::ifstream f("file.txt"); if (f) { std::string str((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>()); // `str` عمليّات على } هذه الطّريقة لا تحتاج شيفرة كبيرة وتسمح بقراءة الملفّ مباشرة إلى أي حاوية مكتبة قياسيّة وليست قاصرة على السّلاسل النصية فقط، لكن قد تكون بطيئة إن كان الملف كبيرُا. ملاحظة: الأقواس الإضافية حول الوسيط الأول في مُنشئ السّلسلة النصية ضرورية لمنع حدوث مشكلة في التحليل (parsing). وأخيرًا وليس آخرًا: std::ifstream f("file.txt"); if (f) { f.seekg(0, std::ios::end); const auto size = f.tellg(); std::string str(size, ' '); f.seekg(0); f.read( & str[0], size); f.close(); // `str` عمليات على } وهو أسرع خيار من بين الخيارات الثلاثة المقترحة. الكتابة في الملفات باستخدام إعدادات محليّة غير قياسية استخدم ‎std::locale‎ و ‎std::basic_ios::imbue()‎ إن كنت بحاجة إلى الكتابة في ملف باستخدام إعدادات محليّة غير الإعدادات الافتراضية، واسترشد بالتوجيهات الآتية: يجب عليك دائمًا تطبيق إعدادات محليّة على المجرى قبل فتح الملف. بمجرد أن تطعِّم المجرى بالإعدادات الجديدة، لا تغيّر الإعدادات المحليّة. أسباب تلك التوجيهات: تحويل مجرى ملف إلى إعدادات محليّة ستكون له تداعيات غير متوقّعه إذا لم تكن الإعدادات الحاليّة مستقلّة، أو إن لم تكن تشير إلى بداية الملف. مجاري UTF-8 وغيرها ليست مستقلة، كما أنّ مجاري الملفّات ذات الإعدادات المحليّة UTF-8 قد تحاول قراءة المِحرف BOM من الملف عند فتحه؛ لذلك فعليك أن تنتبه، فقراءة الملف بعد الفتح قد لا تكون من بدايته دائمًا. #include <iostream> #include <fstream> #include <locale> int main() { std::cout << "User-preferred locale setting is " << std::locale("").name().c_str() << std::endl; // اكتب عدد عشري باستخدام الإعدادات المحليّة للمستخدم std::ofstream ofs1; ofs1.imbue(std::locale("")); ofs1.open("file1.txt"); ofs1 << 78123.456 << std::endl; // استخدم إعدادات محليّة محددة، الأسماء تختلف من نظام لآخر std::ofstream ofs2; ofs2.imbue(std::locale("en_US.UTF-8")); ofs2.open("file2.txt"); ofs2 << 78123.456 << std::endl; // "C" التحوّل إلى الإعدادات المحليّة لـ std::ofstream ofs3; ofs3.imbue(std::locale::classic()); ofs3.open("file3.txt"); ofs3 << 78123.456 << std::endl; } التبديل إلى الإعدادات المحليّة التقليدية للغة "C" مفيد في حال كان البرنامج يستخدم لغة افتراضيّة مختلفة وكنت تريد توحيد معيار القراءة والكتابة. وإن استخدمنا الإعدادات المحليّة المفضّلة في "C"، فإنّ المثال أعلاه سيُكتب هكذا: 78,123.456 78,123.456 78123.456 على سبيل المثال، إذا كانت الإعدادات المحليّة المفضّلة هي اللغة الألمانية ومن ثم فإنها تستخدم تنسيقا مختلفًا للأرقام، فإنّ المثال سيُكتب هكذا: 78 123,456 78,123.456 78123.456 (لاحظ الفاصلة العشرية في السطر الأول). تجنب التحقق من نهاية الملف داخل شرط الحلقة لا تعيد ‎eof‎ القيمة true إلا بعد قراءة نهاية الملف، وهي لا تشير إلى أنّ القراءة التالية ستكون نهاية المجرى. انظر: while (!f.eof()) { // هذا يبدو جيدًا f >> buffer; // صحيحة eof هنا تصبح /* `buffer` استخدم */ } يمكن أن تكتب أيضًا: while (!f.eof()) { f >> buffer >> std::ws; if (f.fail()) break; /* `buffer` استخدم */ } لكنّ الشّيفرة: while (f >> buffer) { /* `buffer` استخدم */ } أبسط وأصحّ. مراجع أخرى: std::ws: تتجاهل المسافة البيضاء في بداية مجرى الدخل. std::basic_ios::fail: تعيد true إذا حدث خطأ في المجرى المرتبط بها. تفريغ المجرى (Flushing a stream) تقوم مجاري الملفات -إضافة إلى العديد من أنواع المجاري الأخرى- بالتخزين المؤقت (buffering) افتراضيًا، وذلك يعني أن الكتابة في المجرى قد لا تؤدي إلى تغير الملف المقابل فورًا، بل يجب أن تفرِّغ المجرى إن أردت ذلك، إما باستدعاء التابع ‎flush()‎ أو عبر معالج المجرى ‎std::flush‎، انظر: std::ofstream os("foo.txt"); os << "Hello World!" << std::flush; char data[3] = "Foo"; os.write(data, 3); os.flush(); يجمع مُعالج المجرى ‎std::endl‎ بين كتابة سطر جديد وتفريغ المجرى، انظر الشيفرة التالية حيث ينفِّذ كلا السطران نفس الشيء: os << "Hello World!\n" << std::flush; os << "Hello world!" << std::endl; يحسّن التخزين المؤقّت (Buffering) أداء عمليات الكتابة في المجرى، لهذا يُفضّل أن تتجنّب التطبيقاتُ كثيرةُ الكتابةِ استخدام التفريع ما لم يكن لازمًا. لكن على العكس من ذلك، إذا لم يكن البرنامج يكثر من عمليات الدّخل والخرج (I/O)، فمن الأفضل الإكثار من التفريغ لتجنب تكدّس البيانات في كائن المجرى. قراءة ملف في حاوية في المثال التالي، سنستخدم ‎std::string‎ و ‎operator>>‎ لقراءة عناصر من ملف. std::ifstream file("file3.txt"); std::vector < std::string > v; std::string s; while (file >> s) // الاستمرار في القراءة حتى النهاية { v.push_back(s); } في المثال أعلاه، كرّرنا -من iterate- على الملف بقراءة "عنصر" واحد في كل مرّة باستخدام العامل <<‎. يمكن تحقيق الشّيء نفسه باستخدام ‎std::istream_iterator‎، والذي هو مكرّر دخْلٍ يقرأ "عنصرًا" واحدًا في كل مرة من المجرى. كذلك يمكن إنشاء معظم الحاويات باستخدام مُكرِّرين كما يلي: std::ifstream file("file3.txt"); std::vector<std::string> v(std::istream_iterator<std::string>{file}, std::istream_iterator<std::string>{}); يمكننا توسيع هذه الشّيفرة لقراءة أيّ نوع من الكائنات من خلال تمرير الكائن الذي نريد قراءته كمعامل قالب (template parameter) إلى ‎std::istream_iterator‎. وهكذا، يمكننا توسيع الشّيفرة السّابقة لقراءة الأسطر (بدلاً من الكلمات). انظر المثال التالي، لاحظ أنه لعدم وجود نوع مضمّن يقرأ الأسطر باستخدام << فإننا سنبني صنفًا مساعدًا لفعل ذلك، وسيقوم بعملية التحويل إلى سلسلة نصية (string) عند استخدامه في سياق نصي: struct Line { // تخزين البيانات هنا std::string data; // تحويل الكائن إلى سلسلة نصية operator std::string const & () const { return data; } // قراءة سطر من المجرى friend std::istream & operator >> (std::istream & stream, Line & line) { return std::getline(stream, line.data); } }; std::ifstream file("file3.txt"); // قراءة أسطر الملف إلى حاوية std::vector<std::string> v(std::istream_iterator<Line>{file}, std::istream_iterator<Line>{}); نسخ الملفات std::ifstream src("source_filename", std::ios::binary); std::ofstream dst("dest_filename", std::ios::binary); dst << src.rdbuf(); الإصدار ≥ C++‎ 17 الطريقة القياسيّة لنسخ ملف في C++‎ 17 هي تضمين التّرويسة ، واستخدام ‎copy_file‎، انظر: std::fileystem::copy_file("source_filename", "dest_filename"); طُوِّرت المكتبة filesystem في البداية كوحدة ‎boost‏‏ (‎boost.filesystem‎)، تم دُمِجت بعد ذلك في التوصيف ISO C++‎ بدءًا من C++‎ 17. إغلاق ملف نادرًا ما يكون عليك إغلاق الملفات في C++‎، لأنّ المجرى سوف يغلق الملف المرتبط به تلقائيًا في المفكك (destructor) الخاص به. ومع ذلك يُفضّل أن تحُدّّ من عمر كائنات المجرى، حتى لا يبقى مِقبض (handle) الملفّ مفتوحًا لفترة أطول من اللازم. على سبيل المثال، يمكنك فعل ذلك عبر وضع جميع العمليّات المُنفّذة على الملف في نطاق خاص (‎{}‎)، لاحظ أن ofstream ستكون خارج النطاق بنهاية هذه الشيفرة: std::string const prepared_data = prepare_data(); { // فتح ملف للكتابة std::ofstream output("foo.txt"); // كتابة البيانات output << prepared_data; } // سيتولى المدمر إغلاق الملف لا يكون استدعاء ‎close()‎ بشكل صريح ضروريًّا إلا إن أردت إعادة استخدام نفس كائن ‎fstream‎ لاحقًا لكنّك لم ترغب في إبقائه مفتوحًا إلى ذلك الحين، انظر المثال التالي الذي نجريه على ملف foo.txt: // افتح الملف لأول مرة std::ofstream output("foo.txt"); // جهِّز بعض البيانات لتكتبها في الملف std::string const prepared_data = prepare_data(); // اكتب البيانات إلى الملف output << prepared_data; // أغلق الملف output.close(); // قد يستغرق تحضير البيانات وقتًا طويلًا // لذا لن نفتح مجرى خرج الملف حتى نكون جاهزين للكتابة فيه std::string const more_prepared_data = prepare_complex_data(); // افتح الملف مرة أخرى بمجرد أن تكون جاهزًا للكتابة فيه output.open("foo.txt"); // اكتب البيانات في الملف output << more_prepared_data; // أغلق الملف مرة أخرى output.close(); قراءة بُنية struct من ملف نصّي منسق الإصدار ≥ C++‎ 11 انظر المثال التالي، سنحدد حملًا زائدًا للعامل <<operator على أنه دالة friend تمنح امتياز الوصول إلى البيانات الخاصة للأعضاء. struct info_type { std::string name; int age; float height; friend std::istream & operator >> (std::istream & is, info_type & info) { // تجاوز المسافة البيضاء is >> std::ws; std::getline(is, info.name); is >> info.age; is >> info.height; return is; } }; void func4() { auto file = std::ifstream("file4.txt"); std::vector < info_type > v; for (info_type info; file >> info;) // اقرأ حتى النهاية { // لن نصل إلى هنا إلا في حال نجاح عملية القراءة v.push_back(info); } for (auto const & info: v) { std::cout << " name: " << info.name << '\n'; std::cout << " age: " << info.age << " years" << '\n'; std::cout << "height: " << info.height << "lbs" << '\n'; std::cout << '\n'; } } ملف file4.txt لنفرض أن البيانات التالية موجودة في ملفّ file4.txt: Wogger Wabbit 2 6.2 Bilbo Baggins 111 81.3 Mary Poppins 29 154.8 إذًا يكون الخرج ما يلي: name: Wogger Wabbit age: 2 years height: 6.2lbs name: Bilbo Baggins age: 111 years height: 81.3lbs name: Mary Poppins age: 29 years height: 154.8lbs هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 12: File I/O‎ من كتاب C++ Notes for Professionals
  8. مجرى الدخل الخاص بالمستخدم ومجرى الخرج القياسي (user input and standard output) #include <iostream> int main() { int value; std::cout << "Enter a value: " << std::endl; std::cin >> value; std::cout << "The square of entered value is: " << value * value << std::endl; return 0; } مجاري التدفق النصية (String streams) std::ostringstream هو صنف كائناته تبدو كمجرى خرْج (output stream)، أي يمكنك الكتابة فيه عبر العامل ‎operator<<‎، ولكنّه في الواقع يخزّن نتائج الكتابة، ويُتيحها على هيئة مجرى. انظر هذا المثال: #include <sstream> #include <string> using namespace std; int main() { ostringstream ss; ss << "the answer to everything is " << 42; const string result = ss.str(); } ينشئ ;ostringstream ss كائنًا مثل هذا الكائن، ويُعامل الكائن أولًا كمجرى عادي: ss << "the answer to everything is " << 42; ثم يمكن الحصول على المجرى الناتج كما يلي: const string result = ss.str(); (السّلسلة النصّية ‎result‎ ستساوي ‎"the answer to everything is 42"‎). ذلك مفيد في حال أردنا التمثيل النصّي لصنف سبق تعريف تسلسلِ مجراه (stream serialization). على سبيل المثال، لنفترض أنّ لدينا الصنف التالي: class foo { // التعليمات والأوامر البرمجية ستكون هنا. }; ostream &operator<<(ostream &os, const foo &f); للحصول على التمثيل النصي للكائن ‎foo‎: foo f; يمكننا استخدام: ostringstream ss; ss << f; const string result = ss.str(); وعليه ستحتوي ‎result‎ التمثيل النصّي للكائن ‎foo‎. طباعة المجموعات باستخدام iostream الطباعة الأساسية (Basic printing) تتيح std::ostream_iterator طباعة محتويات حاويات مكتبة القوالب القياسية STL في أيّ مَجرى خرج دون الحاجة إلى استخدام الحلقات، والوسيط الثاني المُمرَّر إلى مُنشِئ ‎std::ostream_iterator‎ يعين المحدِّد (delimiter). انظر المثال التالي: std::vector<int> v = {1,2,3,4}; std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " ! ")); سوف يطبع: 1 ! 2 ! 3 ! 4 ! التحويل الضمني للأنواع (Implicit type cast) تتيح std::ostream_iteratorتحويل (casting) نوع محتوى الحاوية ضمنيًا. في المثال التالي، ستطبع std::cout أعدادًا عشريّة ذات ثلاث منازل عشرية: std::cout << std::setprecision(3); std::fixed(std::cout); وستستنسخ‏‏ ‎std::ostream_iterator‎ باستخدام النوع ‎float‎، بينما ستظلّ القيم المُضمّنة قيمًا عددية صحيحة (‎int‎): std::vector<int> v = {1,2,3,4}; std::copy(v.begin(), v.end(), std::ostream_iterator<float>(std::cout, " ! ")); تعيد الشّيفرة أعلاه النتيجة التالية على الرغم من أنّ المتجهة (‎std::vector‎) تحتوي أعدادًا صحيحة. 1.000 ! 2.000 ! 3.000 ! 4.000 ! التوليد والتحويل تشكّل دّوال std::generate و std::generate_n و std::transform أداة فعّالة لمعالجة البيانات، فمثلًا، انظر المتجهة التالية: std::vector<int> v = {1,2,3,4,8,16}; نستطيع باستخدامها أن نطبع القيمة البوليانية لتعليمة x is even للأعداد الزوجية بسهولة، كما يلي: std::boolalpha(std::cout); // طباعة القيم البوليانية أبجديًا std::transform(v.begin(), v.end(), std::ostream_iterator<bool>(std::cout, " "), [](int val) { return (val % 2) == 0; }); أو طباعة مربعات العناصر: std::transform(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "), [](int val) { return val * val; }); أو طباعة N عدد عشوائي تفصلهم مسافة فارغة: const int N = 10; std::generate_n(std::ostream_iterator<int>(std::cout, " "), N, std::rand); المصفوفات يمكن تطبيق جميع هذه الاعتبارات تقريبًا على المصفوفات الأصلية، تمامًا كما هو الحال في القسم الخاص بقراءة الملفات النصية. انظر المثال التالي حيث نطبع القيم التربيعيّة لعناصِر مصفوفة أصلية: int v[] = {1,2,3,4,8,16}; std::transform(v, std::end(v), std::ostream_iterator<int>(std::cout, " "), [](int val) { return val * val; }); معالِجات مجاري التدفق معالِجات مجاري التدفق (Stream manipulators) هي دوال خاصّة مساعِدة للتحكّم في مجاري الدخل والخرج باستخدام العاملين ‎operator >>‎ و ‎operator<<‎. ويمكن تضمينها عبر ‎#include <iomanip>‎. تُبادِل std::boolalpha و std::noboolalpha بين التمثيل النصي والعددي للقيَم البوليانية. انظر المثال التالي: std::cout << std::boolalpha << 1; // true الخرج std::cout << std::noboolalpha << false; // 0 الخرج bool boolValue; std::cin >> std::boolalpha >> boolValue; std::cout << "Value \"" << std::boolalpha << boolValue << "\" was parsed as " << std::noboolalpha << boolValue; // true الدخل // تحليل قيمة الدخل على أنها 0 تحدد كل من std::showbase و std::noshowbase إن تم استخدام السابقة (Prefix) التي تشير إلى الأساس العادي. تُستخدم كل من std::dec (عشري) و std::hex (ست عشري) و std::oct (ثماني) من أجل تغيير الأساس العددي للأعداد الصحيحة. #include <sstream> std::cout << std::dec << 29 << ' - ' << std::hex << 29 << ' - ' << std::showbase << std::oct << 29 << ' - ' << std::noshowbase << 29 '\n'; int number; std::istringstream("3B") >> std::hex >> number; std::cout << std::dec << 10; // الخرج // 22 - 1D - 35 - 035 // 59 القيم الافتراضية هي ‎std::ios_base::noshowbase‎ و ‎std::ios_base::dec‎. يمكنك معرفة المزيد حول ‎std::istringstream‎ من الترويسة <sstream> تحدد كل من std::uppercase و std::nouppercase ما إذا كانت الأحرف الكبيرة ستُستخدم في خرْج الأعداد العشريّة والأعداد الست عشرية، وليس لها أيّ تأثير على مجاري الدخل. std::cout << std::hex << std::showbase << "0x2a with nouppercase: " << std::nouppercase << 0x2a << '\n' << "1e-10 with uppercase: " << std::uppercase << 1e-10 << '\n'; // الخرج: // 0x2a with nouppercase: 0x2a // 1e-10 with uppercase: 1E-10 القيمة الافتراضيّة هي std::nouppercase. تغيّر std::setw(n)‎ - عرض (width) حقل الدخل/الخرج التالي إلى القيمة ‎n‎، وتتم إعادة تعيين خاصية العرض ‎n‎ إلى ‎0‎ عند استدعاء بعض الدّوال (انظر القائمة الكاملة). std::cout << "no setw:" << 51 << '\n' << "setw(7): " << std::setw(7) << 51 << '\n' << "setw(7), more output: " << 13 << std::setw(7) << std::setfill('*') << 67 << ' ' << 94 << '\n'; char* input = "Hello, world!"; char arr[10]; std::cin >> std::setw(6) >> arr; std::cout << "Input from \"Hello, world!\" with setw(6) gave \"" << arr << "\"\n"; // الخرج: // 51 // setw(7): 51 // setw(7): more output: 13*****67 94 // Hello, world! :الدخل // "Hello" تعيد setw(6) حيث "Hello, world!" الخرج: الدخل من لاحظ أن الخرج في الشيفرة السابقة سيكون ("Input from "Hello, world!" with setw(6) gave "Hello). وتكون القيمة الافتراضية std::setw(0)‎. تُعدِّل كل من std::left و std::right و std::internal الموضعَ الافتراضي لمحارف الملء (fill characters) من خلال ضبط std::ios_base::adjustfield إلىstd::ios_base::leftو std::ios_base::right و std::ios_base::internal على الترتيب. كذلك، تُطبَّق std::left و std::right على أيّ خرج، كما تُطبّق std::internal على الأعداد الصحيحة والعشرية والنقديّة، وليس لها أيّ تأثير على مجاري الدخل. #include <locale> ... std::cout << std::left << std::showbase << std::setfill('*') << "flt: " << std::setw(15) << -9.87 << '\n' << "hex: " << std::setw(15) << 41 << '\n' << " $: " << std::setw(15) << std::put_money(367, false) << '\n' << "usd: " << std::setw(15) << std::put_money(367, true) << '\n' << "usd: " << std::setw(15) << std::setfill(' ') << std::put_money(367, false) << '\n'; // :الخرج // flt: -9.87********** // hex: 41************* // $: $3.67********** // usd: USD *3.67****** // usd: $3.67 std::cout << std::internal << std::showbase << std::setfill('*') << "flt: " << std::setw(15) << -9.87 << '\n' << "hex: " << std::setw(15) << 41 << '\n' << " $: " << std::setw(15) << std::put_money(367, false) << '\n' << "usd: " << std::setw(15) << std::put_money(367, true) << '\n' << "usd: " << std::setw(15) << std::setfill(' ') << std::put_money(367, true) << '\n'; // الخرج // flt: -**********9.87 // hex: *************41 // $: $3.67********** // usd: USD *******3.67 // usd: USD 3.67 std::cout << std::right << std::showbase << std::setfill('*') << "flt: " << std::setw(15) << -9.87 << '\n' << "hex: " << std::setw(15) << 41 << '\n' << " $: " << std::setw(15) << std::put_money(367, false) << '\n' << "usd: " << std::setw(15) << std::put_money(367, true) << '\n' << "usd: " << std::setw(15) << std::setfill(' ') << std::put_money(367, true) << '\n'; // الخرج // flt: **********-9.87 // hex: *************41 // $: **********$3.67 // usd: ******USD *3.67 // usd: USD 3.67 القيمة الافتراضية هي ‎std::left‎. تغير كل من std::fixed و std::scientific و std::hexfloat ‏‏[C++11] و std::defaultfloat ‏‏[C++11] تنسيقَ الدخل والخرج للأعداد العشريّة. std::fixed تعيّن std::ios_base::floatfield إلى std::ios_base::fixed. تعيّن std::scientific إلى std::ios_base::scientific. تعيّن std::hexfloat إلى ‎‎std::ios_base::fixed | std::ios_base::scientific وتعيّن std::defaultfloat إلى std::ios_base::fmtflags(0)‎. إليك الشيفرة التالية: #include <sstream> ... std::cout << '\n' << "The number 0.07 in fixed: " << std::fixed << 0.01 << '\n' << "The number 0.07 in scientific: " << std::scientific << 0.01 << '\n' << "The number 0.07 in hexfloat: " << std::hexfloat << 0.01 << '\n' << "The number 0.07 in default: " << std::defaultfloat << 0.01 << '\n'; double f; std::istringstream is("0x1P-1022"); double f = std::strtod(is.str().c_str(), NULL); std::cout << "Parsing 0x1P-1022 as hex gives " << f << '\n'; // الخرج // 0.070000 // 7.000000e-02 // 0x1.1eb851eb851ecp-4 // 0.07 // 2.22507e-308 القيمة الافتراضية هي ‎ ‎‎std::ios_base::fmtflags(0)‎‎. هناك خلل في بعض المصرّفات يؤدي إلى النتيجة التالية: double f; std::istringstream("0x1P-1022") >> std::hexfloat >> f; std::cout << "Parsing 0x1P-1022 as hex gives " << f << '\n'; // الخرج: // Parsing 0x1P-1022 as hex gives 0 تتحكم كل من std::showpoint و std::noshowpoint فيما إذا كانت الفاصلة العشرية ستُضمَّن دائمًا في تمثيل الفاصلة العائمة (floating-point representation)، وهما ليس لهما أي تأثير على مجاري الدخل. std::cout << "7.0 with showpoint: " << std::showpoint << 7.0 << '\n' << "7.0 with noshowpoint: " << std::noshowpoint << 7.0 << '\n'; النّاتج سيكون: 1.0 with showpoint: 7.00000 1.0 with noshowpoint: 7 القيمة الافتراضية هي std::showpoint. تتحكم كل من std::showpos و std::noshowpos فيما إذا كانت العلامة ‎+‎ ستُعرض في الخرج عند عرض الأعداد الموجبة، وهما كذلك ليس لهما أيّ تأثير على مجاري الدخل. std::cout << "With showpos: " << std::showpos << 0 << ' ' << -2.718 << ' ' << 17 << '\n' << "Without showpos: " << std::noshowpos << 0 << ' ' << -2.718 << ' ' << 17 << '\n'; // الخرج: // With showpos: +0 -2.718 +17 // Without showpos: 0 -2.718 17 القيمة الافتراضية ‎std::noshowpos‎. تتحكم كل من std::unitbuf و std::nounitbuf في تفريغ مجرى الخرج بعد كل عملية، وهما كذلك ليس لهما أي تأثير على مجاري الدخل. تنفّذ ‎std::unitbuf‎ عمليّة التفريغ. يحدّد std::setbase(base)‎ الأساس العددي (numeric base) للمجرى. std::setbase(8)‎ تكافئ تعيين std::ios_base::basefield إلى ‎std::ios_base::oct‎ وتعيين std::setbase(16)‎ إلى std::ios_base::hex وتعيين std::setbase(10)‎ إلى ‎std::ios_base::dec‎. إن كان الأساس العدديّ مخالفًا لـ 8 و10 و 16، فستُعيَّن std::ios_base::basefield إلى std::ios_base::fmtflags(0)‎، وذلك يعني أنّ الخرج سيكون عشريًا، أمّا الدخل فسيتعلّق بالأساس العددي (prefix-dependent). القيمة الافتراضيّة لـ ‎std::ios_base::basefield‎ هي ‎std::ios_base::dec‎، لذلك، فافتراضيًا سيكون لدينا الأساس العشري ‎std::setbase(10)‎ تغيّر std::setprecision(n)‎ دقةَ الأعداد العشرية. #include <cmath> #include <limits> ... typedef std::numeric_limits < long double > ld; const long double pi = std::acos(-1. L); std::cout << '\n' << "default precision (6): pi: " << pi << '\n' << " 10pi: " << 10 * pi << '\n' << "std::setprecision(4): 10pi: " << std::setprecision(4) << 10 * pi << '\n' << " 10000pi: " << 10000 * pi << '\n' << "std::fixed: 10000pi: " << std::fixed << 10000 * pi << std::defaultfloat << '\n' << "std::setprecision(10): pi: " << std::setprecision(10) << pi << '\n' << "max-1 radix precicion: pi: " << std::setprecision(ld::digits - 1) << pi << '\n' << "max+1 radix precision: pi: " << std::setprecision(ld::digits + 1) << pi << '\n' << "significant digits prec: pi: " << std::setprecision(ld::digits10) << pi << '\n'; // الخرج // pi: 3.14159 // 10pi: 31.4159 // 10pi: 31.42 // 10000pi: 3.142e+04 // std::fixed: 10000pi: 31415.9265 // std::setprecision(10): pi: 3.141592654 // max-1 radix precicion: pi: 3.14159265358979323851280895940618620443274267017841339111328125 // max+1 radix precision: pi: 3.14159265358979323851280895940618620443274267017841339111328125 // pi: 3.14159265358979324 القيمة الافتراضية هي ‎std::setprecision(6)‎. تعين كل من std::setiosflags(mask)‎ و std::resetiosflags(mask)‎ - الرايات (flags) المحدّدة في العنصر ‎mask‎ ذي النّوع std::ios_base::fmtflags، وتمسحها كذلك. #include <sstream> ... std::istringstream in("10 010 10 010 10 010"); int num1, num2; in >> std::oct >> num1 >> num2; std::cout << "Parsing \"10 010\" with std::oct gives: " << num1 << ' ' << num2 << '\n'; // الخرج: // Parsing "10 010" with std::oct gives: 8 8 in >> std::dec >> num1 >> num2; std::cout << "Parsing \"10 010\" with std::dec gives: " << num1 << ' ' << num2 << '\n'; // الخرج: // Parsing "10 010" with std::oct gives: 10 10 in >> std::resetiosflags(std::ios_base::basefield) >> num1 >> num2; std::cout << "Parsing \"10 010\" with autodetect gives: " << num1 << ' ' << num2 << '\n'; // الخرج: // Parsing "10 010" with std::oct gives: 10 8 std::cout << std::setiosflags(std::ios_base::hex | std::ios_base::uppercase | std::ios_base::showbase) << 42 << '\n'; // الخرج: OX2A تحدد كل من std::skipws و std::noskipws ما إذا كانت دوال الدّخل المُنسّق (formatted input functions) ستتجاوز المسافة البادئة الفارغة. وليس لهما أي تأثير على مجاري الخرج. #include <sstream> ... char c1, c2, c3; std::istringstream("a b c") >> c1 >> c2 >> c3; std::cout << "Default behavior: c1 = " << c1 << " c2 = " << c2 << " c3 = " << c3 << '\n'; std::istringstream("a b c") >> std::noskipws >> c1 >> c2 >> c3; std::cout << "noskipws behavior: c1 = " << c1 << " c2 = " << c2 << " c3 = " << c3 << '\n'; // الخرج: // Default behaviour: c1 = a c2 = b c3 = c // noskipws behavior: c1 = a c2 = c3 = b القيمة الافتراضية هي ‎std::ios_base::skipws‎. std::quoted(s[, delim[, escape]])‎‏ [C++14] تدرج أو تستخرج السّلاسل النصية المُقتبسة (quoted) ذات المسافات الفارغة المُدمجة. s - السّلسلة النصية المرادُ إدراجها أو استخلاصها. delim - المحرف المراد استخدامه كمحدّد (delimiter)، القيمة الافتراضية هي ". escape - المحرف المستخدم كحرف تهريب (escape character) ، القيمة الافتراضيّة هي \. #include <sstream> ... std::stringstream ss; std::string in = "String with spaces, and embedded \"quotes\" too"; std::string out; ss << std::quoted( in ); std::cout << "read in [" << in << "]\n" << "stored as [" << ss.str() << "]\n"; ss >> std::quoted(out); std::cout << "written out [" << out << "]\n"; // الخرج // [String with spaces, and embedded "quotes" too] تُقرأ كـ // ["String with spaces, and embedded \"quotes\" too"] تُخزّن كـ // [String with spaces, and embedded "quotes" too] تُكتب كـ معالِجات مجاري الخرج std::ends - تُدرج محرفًا فارغًا '‎\0' في مجرى الخرْج. تبدو الصيغة الرسمية لهذا المعالِج كالتالي: template <class charT, class traits> std::basic_ostream<charT, traits>& ends(std::basic_ostream<charT, traits>& os); هذا المعالِجُ يُدرج المحرفَ الفارغَ عبر استدعاء ‎os.put(charT())‎ عند استخدامه في تعبير ‎os << std::ends;‎ يفرِّغ كلا من std::endl و std::flush مجرى الإخراج ‎out‎ عن طريق استدعاء out.flush()‎، ممّا يؤدّي إلى عرض الخرج على الفور. لكنّ ‎std::endl‎ تدرج رمز نهاية السّطر ‎'\n'‎ قبل التفريغ. انظر: std::cout << "First line." << std::endl << "Second line. " << std::flush << "Still second line."; الناتج: First line. Second line. Still second line. std::setfill(c)‎ - تغيّر محرف الملء (fill character) إلى ‎c‎، تُستخدم غالبًا مع std::setw. std::cout << "\nDefault fill: " << std::setw(10) << 79 << '\n' << "setfill('#'): " << std::setfill('#') << std::setw(10) << 42 << '\n'; // الخرج: // Default fill: 79 // setfill('#'): ########79 std::put_money(mon[, intl])‎‏‏ [C++11] - تُحوِّل القيمة النقدية ‎mon‎ (من النوع ‎long double‎ أو ‎std::basic_string‎ ) في التعبير ‎out << std::put_money(mon, intl)‎ إلى تمثيلها المحرفيّ كما هو محدد في std::money_put في الإعدادات المحلّية الخاصّة بالعملة الموجودة في ‎out‎. استخدم سلاسل العملات الدولية إذا كانت ‎intl‎ تساوي true وإلا فاستخدم رموز العملات. long double money = 123.45; // std::string money = "123.45"; :أو std::cout.imbue(std::locale("en_US.utf8")); std::cout << std::showbase << "en_US: " << std::put_money(money) << " or " << std::put_money(money, true) << '\n'; // en_US: $1.23 or USD 1.23 :الخرج std::cout.imbue(std::locale("ru_RU.utf8")); std::cout << "ru_RU: " << std::put_money(money) << " or " << std::put_money(money, true) << '\n'; // ru_RU: 1.23 руб or 1.23 RUB :الخرج std::cout.imbue(std::locale("ja_JP.utf8")); std::cout << "ja_JP: " << std::put_money(money) << " or " << std::put_money(money, true) << '\n'; // ja_JP: ¥123 or JPY 123 :الخرج std::put_time(tmb, fmt)‎‏‏ [C++11] - تُنسّق وتُخرِج قيمة تقويم تاريخ/وقت إلى std::tm بحسب تنسيق fmt. tmb - مؤشّر إلى بنية التقويم (calendar time structure)‏‏ const std::tm*‎ كما أعادته localtime()‎ أو gmtime()‎. fmt - مؤشّر إلى سلسلة نصّية منتهية بقيمة فارغة const CharT*‎ تمثّل تنسيق التحويل. #include <ctime> ... std::time_t t = std::time(nullptr); std::tm tm = *std::localtime(&t); std::cout.imbue(std::locale("ru_RU.utf8")); std::cout << "\nru_RU: " << std::put_time(&tm, "%c %Z") << '\n'; // الخرج الممكن // ru_RU: Вт 04 июл 2017 15:08:35 UTC معالِجات مجاري الدخل (Input stream manipulators) تستهلك std::ws المسافات البادئة في مجرى الدخل، وهي مختلفة عن std::skipws. #include <sstream> ... std::string str; std::istringstream(" \v\n\r\t Wow!There is no whitespaces!") >> std::ws >> str; std::cout << str; // الخرج // Wow!There is no whitespaces! std::get_money(mon[, intl])‎‏ [C++11] - في تعبير in >> std::get_money(mon, intl)‎، تحلّل دخل المحارف كقيمة نقديّة كما هو محدّد بواسطة std::money_get في الإعدادات المحلية في in، وتخزّن القيمة في mon (من نوع long double أو std::basic_string). سيتوقّع المعالِج سلاسل العملات الدوليّة المطلوبة إذا كانت intl تساوي true، وإلا فسَيتوقع رموز عملات اختيارية. #include <sstream> #include <locale> ... std::istringstream in("$1,234.56 2.22 USD 3.33"); long double v1, v2; std::string v3; in.imbue(std::locale("en_US.UTF-8")); in >> std::get_money(v1) >> std::get_money(v2) >> std::get_money(v3, true); if (in) { std::cout << std::quoted(in.str()) << " parsed as: " << v1 << ", " << v2 << ", " << v3 << '\n'; } // الخرج // "$1,234.56 2.22 USD 3.33" parsed as: 123456, 222, 333 تحلّل std::get_time(tmb, fmt)‎ ‏‏[C++11] قيمة التاريخ/الوقت المُخزّنة في ‎tmb‎ بتنسيق ‎fmt‎ المحدد. ‎tmb‎ - مؤشّر صالح للكائن const std::tm*‎ حيث ستُخزَّن النتيجة. ‎fmt‎ - مؤشّر إلى سلسلة نصيّة منتهية بقيمة فارغة const CharT*‎ تمثّل تنسيق التحويل. #include <sstream> #include <locale> ... std::tm t = {}; std::istringstream ss("2011-Februar-18 23:12:34"); ss.imbue(std::locale("de_DE.utf-8")); ss >> std::get_time(&t, "%Y-%b-%d %H:%M:%S"); if (ss.fail()) { std::cout << "Parse failed\n"; } else { std::cout << std::put_time(&t, "%c") << '\n'; } // الخرج // Sun Feb 18 23:12:34 2011 هذا الدرس جزء من سلسلة مقالات عن C++‎. ترجمة -بتصرّف- للفصل Chapter 13: C++ Streams والفصل Chapter 14: Stream manipulators من كتاب C++ Notes for Professionals
  9. تنفذ الحلقات التكرارية مجموعة من التعليمات إلى حين استيفاء شرط معين، وهناك ثلاثة أنواع من تلك الحلقات التكرارية في لغة C++: for و while و do…while. حلقة for النطاقية (Range-Based For) الإصدار ≥ C++‎ 11 يمكن استخدام حلقات for للتكرار على عناصر نطاق تكراري (iterator-based range) دون الحاجة إلى استخدام الفهارس العددية أو الوصول بشكل مباشر إلى المكررات: vector < float > v = { 0.4 f, 12.5 f, 16.234 f }; for (auto val: v) { std::cout << val << " "; } std::cout << std::endl; ستكرّر الشيفرة السابقة تعليمة std::cout << val << " ";‎ على كل عناصر v، وستحصل val على قيمة العنصر الحالي. الشيفرة التالية: for (for-range-declaration : for-range-initializer ) statement ستكون مكافئة لما يلي: { auto&& __range = for-range-initializer; auto __begin = begin-expr, __end = end-expr; for (; __begin != __end; ++__begin) { for-range-declaration = *__begin; statement } } الإصدار ≥ C++‎ 17 من الممكن أن تكون end من نوع مختلف عن begin في C++17، انظر: { auto&& __range = for-range-initializer; auto __begin = begin-expr; auto __end = end-expr; for (; __begin != __end; ++__begin) { for-range-declaration = *__begin; statement } } قُدِّم هذا التغيير في C++‎ لأجل التمهيد لدعم معيار النطاقات Ranges TS في الإصدار C++‎ 20، وعليه فإن الحلقة في هذه الحالة ستكون مكافئة لما يلي: { auto &&__range = v; auto __begin = v.begin(), __end = v.end(); for (; __begin != __end; ++__begin) { auto val = *__begin; std::cout << val << " "; } } لاحظ أن التعليمة auto val تصرح عن نوع القيمة التي ستكون نسخة من القيمة المخزنة داخل النطاق – سنهيئه استنساخًا أثناء تنفيذ البرنامج-، وإن كانت عملية نسخ القيم المخزنة في النطاق مكلفة فربما تود استخدام const auto &val. كذلك لست مضطرًا لاستخدام auto ، بل تستطيع استخدام أي اسم نوع typename طالما أنه قابل للتحويل من نوع قيمة النطاق. واعلم أن حلقة for النطاقية غير مناسبة في حال احتجت للوصول إلى المُكرّر، أو ستحتاج مجهودًا كبيرًا. إن أردت الإشارة للمكرِّر، فيمكنك ذلك بما يلي: vector < float >v = {0.4f, 12.5f, 16.234f}; for(float &val: v) { std::cout << val << " "; } كذلك يمكنك التكرار على مرجع const إذا كان لديك حاوية const: const vector<float> v = {0.4f, 12.5f, 16.234f}; for(const float &val: v) { std::cout << val << " "; } يمكنك استخدام مراجع إعادة التوجيه (forwarding references) في حال أعاد مُكرّر التسلسل كائنًا وكيلًا (proxy object) وكنت بحاجة إلى إجراء عمليات يمكن أن تغيّر قيمة الكائن. لكن بالمقابل، من المحتمل أن تربك من يقرأ شيفرتك إن استخدمت هذه الطريقة. vector<bool> v(10); for (auto &&val : v) { val = true; } يمكن أن يكون نوع "النطاق" المقدم إلى حلقة for النطاقية أحد الخيارات التالية: المصفوفات: float arr[] = {0.4f, 12.5f, 16.234f}; for (auto val : arr) { std::cout << val << " "; } لاحظ أنّ تخصيص المصفوفات الديناميكية لا يُحسب: float *arr = new float[3]{0.4f, 12.5f, 16.234f}; for (auto val : arr) // خطأ تصريفي { std::cout << val << " "; } أي نوع يحتوي على دوال begin()‎ و end()‎ تابعةٍ، يعيدُ مكرّرات إلى عناصر من ذلك النوع، ويمكنك استخدام حاويات المكتبات القياسية إضافة إلى استخدام الأنواع التي يحددها المستخدم (User-Defined)، انظر: struct Rng { float arr[3]; // المؤشرات ما هي إلا مكرّرات const float * begin() const { return &arr[0]; } const float * end() const { return &arr[3]; } float * begin() { return &arr[0]; } float * end() { return &arr[3]; } }; int main() { Rng rng = { { 0.4f, 12.5f, 16.234f } }; for (auto val: rng) { std::cout << val << " "; } } أيّ نوع لديه دوال begin(type)‎ و end(type)‎ غير تابعة (non-member)، يمكن إيجاده من خلال البحث بالوسائط (Arguments) استنادًا للنوع type، هذا مفيد في إنشاء نوع نطاقٍ (range type) دون الحاجة إلى تعديل نوع الصنف نفسه، انظر: namespace Mine { struct Rng { float arr[3]; }; // المؤشرات ما هي إلا مكررات const float * begin(const Rng & rng) { return &rng.arr[0]; } const float * end(const Rng & rng) { return &rng.arr[3]; } float * begin(Rng & rng) { return &rng.arr[0]; } float * end(Rng & rng) { return &rng.arr[3]; } } int main() { Mine::Rng rng = { { 0.4f, 12.5f, 16.234f } }; for (auto val: rng) { std::cout << val << " "; } } حلقة for التكرارية تنفّذ الحلقة for التعليمات الموجودة في متن الحلقة loop body ما دامَ شَرط الحلقة condition صحيحًا، وتُنفَّذ initialization statement مرة واحدة قبل تنفيذ الحلقة التكرارية، ثم تُنفّذ التعليمة iteration execution بعد كل تكرار. تٌعرَّف حلقة for كما يلي: for (/*initialization statement*/; /*condition*/; /*iteration execution*/) { // متن الحلقة } شرح الصيغة السابقة: تُنفَّذ تعليمة initialization statement مرة واحدة فقط في بداية حلقة for، وتستطيع هنا أن تصرِّح عن عدة متغيرات من نفس النوع، مثل int i = 0, a = 2, b = 3. لا تكون تلك المتغيرات صالحة إلا في نطاق الحلقة، وأما المتغيرات التي لها نفس الاسم وعُرِّفّت قبل تنفيذ الحلقة فإنها تُخفى أثناء تنفيذ الحلقة. تُقيَّم تعليمة condition قبل كل مرة يُنفَّذ فيها متن الحلقة (loop body) كي تُوقف الحلقة إن أعادت القيمة false. تُنفَّذ تعليمة iteration execution بعد تنفيذ متن الحلقة وقبل تقييم الشرط التالي إلا إن أُوقِفت حلقة for في المتن بواسطة break أو goto أو return، أو في حالة رفع اعتراض (throwing an exception). تستطيع وضع عدة تعليمات في الجزء iteration execution مثل a++, b+=10, c=b+a. تكافئ الشيفرةُ التالية حلقةَ for في حال كتابتها كحلقة while، حيث تنتقل الحلقة إلى جزء تنفيذ التكرار /*iteration execution*/ عند استخدام continue: /*initialization*/ while ( /*condition*/ ) { // متن الحلقة /*iteration execution*/ } غالبًا ما تُستخدم الحلقة for لتنفيذ تعليمات برمجية معيّنة عددًا محدّدًا من المرات. على سبيل المثال: for (int i = 0; i < 10; i++) { std::cout << i << std::endl; } أو: for (int a = 0, b = 10, c = 20; (a + b + c < 100); c--, b++, a += c) { std::cout << a << " " << b << " " << c << std::endl; } هذا مثال يوضّح إخفاء المتغيرات المُصرّح عنها قبل الحلقة، حيث نصرح في السطر الثاني عن المتغير i الذي ستتغير قيمته بين 0 و 9 أثناء تنفيذ الحلقة، ثم نستطيع يعود إلى قيمته الأصلية 99 بعد انتهاء تنفيذها. int i = 99; //i = 99 for (int i = 0; i < 10; i++) { // i التصريح عن متغير جديد } ولكن إن كنت تريد استخدام المتغيرات المُصرّحة سلفًا وعدم إخفائها، فاحذف التصريح. انظر المثال التالي إذ نستخدم متغير i المصرَّح عنه من قبل، والذي تتغير قيمته بين 0 و 9 أثناء تنفيذ الحلقة، لكن هذه المرة ستكون قيمته بعد تنفيذها 10. int i = 99; //i = 99 for (i = 0; i < 10; i++) { // i سنستخدم المتغير المُعرّف مسبقا } ملاحظات: يمكن لتعليمات التهيئة (initialization) والزيادة (increment) إجراء عمليات لا تتعلق بتعليمة شرط الحلقة، بل قد تكون فارغة تمامًا إن شئت ذلك، لكن من الأفضل أن نجري عمليات لها علاقة مباشرة بالحلقة لجعل الشيفرة أسهل في القراءة والفهم. لا يُرى المتغير الذي صُرِّح عنه في تعليمة التهيئة إلا داخل نطاق حلقة for، ويُحذف بمجرد إنهائها. لا تنس أنّ المتغير الذي تم التصريح عنها في initialization statement يمكن أثناء الحلقة، وكذلك المتغير المُحدّد في الشرط condition. انظر المثال التالي لحلقة تَعُدُّ من 0 إلى 10: for (int counter = 0; counter <= 10; ++counter) { std::cout << counter << '\n'; } // counter لا يمكن الوصول هنا إلى شرح الشيفرة السابقة: التعليمة int counter = 0 تهيّئ المتغير counter عند القيمة 0. لا يمكن استخدام هذا المتغير إلا داخل حلقة for. التعليمة ‎counter <= 10 هي شرط بولياني يتحقق ممّا إذا كان counter أصغر من أو يساوي 10. إذا كان الشرط صحيحًا (true) فستُنفّذ الحلقة. وإن كان false فستُنهى. counter++‎ هي عملية زيادة (increment) تزيد قيمة العدَّاد بـ 1 قبل التحقق من الشرط التالي. إن تركت جميع التعليمات فارغة، فستحصل على حلقة لا نهائية (infinite loop): for (;;) std::cout << "Never ending!\n"; حلقة while اللانهائية التي تكافئ حلقة for السابقة هي: while (true) std::cout << "Never ending!\n"; لكن على أي حال، يمكن إيقاف الحلقات اللانهائية باستخدام تعليمات break أو goto أو return، أو عبر رفع اعتراض (throwing an exception). انظر المثال التالي الذي يوضح التكرار على عناصر مجموعة من المكتبة القياسية STL (مثل vector) دون استخدام الترويسة : std::vector < std::string > names = { "Albert Einstein", "Stephen Hawking", "Michael Ellis" }; for (std::vector < std::string > ::iterator it = names.begin(); it != names.end(); ++it) { std::cout << * it << std::endl; } حلقة While تكرِّر حلقة while تنفيذ تعليمات معيّنة طالما كان شرطها صحيحًا، تُستخدم هذه الحلقة إن لم تكن تعرف عدد مرات تنفيذ جزء من الشيفرة بشكل مسبق. فمثلأ، لطباعة جميع الأعداد من 0 إلى 9، يمكن استخدام الشيفرة التالية: int i = 0; while (i < 10) { std::cout << i << " "; ++i; // عداد الزيادة } std::cout << std::endl; تُطبع الأعداد من 0 إلى 9 مع نهاية السطر الأخير. لاحظ أن دمج التعليمتيْن الأوليين صار ممكنًا منذ الإصدار C++ 17، انظر: while (int i = 0; i < 10) //... بقية الشيفرة هي نفسها يمكن استخدام الشيفرة التالية لإنشاء حلقة لا نهائية، وتستطيع إيقاف الحلقة عبر تعليمة break: while (true) { // أدخل هنا أي شيء تريد فعله بلا نهاية } هناك صورة آخرى لحلقات while، وهي do...while. وهي موضوع الفقرة التالية. حلقة do-while الحلقتان التكراريتان do-while و while متشابهتان، إلا أن الأولى تتحقق من الشرط في نهاية كل تكرار، وليس في بدايته، وعليه فإنّ الحلقة ستُنفّذ مرة واحدة على الأقل. ستطبع الشيفرة التالية العدد 0 إذ سيكون تقييمُ الشرط false في نهاية التكرار الأول: int i = 0; do { std::cout << i; ++i; // عداد الزيادة } while (i < 0); std::cout << std::endl; // يطبع صفرًا تنبيه: لا تنس الفاصلة المنقوطة في نهاية while(condition);‎، فهي إلزامية في بنية do-while. على النقيض من الحلقة do-while، فإن الشيفرة التالية لن تطبع شيئًا لأنّ شرط الحلقة لم يتحقق في بداية التكرار الأول: int i = 0; while (i < 0) { std::cout << i; ++i; // عداد الزيادة } std::cout << std::endl; // لن يُطبع أيّ شيء تنبيه: يمكن إنهاء الحلقة while حتى لو لم يصبح الشرط خاطئًا باستخدام أي من التعليمات الآتية: break أو goto أو return. int i = 0; do { std::cout << i; ++i; // عداد الزيادة if (i > 5) { break; } } while (true); std::cout << std::endl; // يطبع الأعداد من صفر إلى خمسة تُستخدم الحلقة do-while أحيانًا لكتابة وحدات الماكرو (macros) التي تتطلّب نطاقًا خاصًّا بها (في هذه الحالة، يتمّ حذف الفاصلة المنقوطة الزائدة من تعريف الماكرو، ويُطلب من المستخدم توفيرها): #define BAD_MACRO(x) f1(x); f2(x); f3(x); // f1 الشرط لا يحمي هنا إلا استدعاء if (cond) BAD_MACRO(var); #define GOOD_MACRO(x) do { f1(x); f2(x); f3(x); } while(0) // كل الاستدعاءات محميّة هنا if (cond) GOOD_MACRO(var); تعليمات التحكم في الحلقات: break و continue تُستخدم عبارتَا التحكم break و continue لتغيير مسار التنفيذ من تسلسله المعتاد، فتُدمَّر جميع الكائنات الآلية (automatic objects) التي أنشئت داخل نطاق ما بمجرد ترك تنفيذٍ لذلك النطاق. وتنهي التعليمة break الحلقة فورًا دون النظر لأي عوامل أخرى. for (int i = 0; i < 10; i++) { if (i == 4) break; // إنهاء الحلقة فورا std::cout << i << '\n'; } تكون النتيجة ما يلي: 1 2 3 أما التّعليمة continue لا توقف الحلقة على الفور، بل تتخطّى بقيّة التعليمات الموجودة في متن الحلقة وتذهب إلى بداية الحلقة (بما في ذلك تعليمة التحقّق من الشّرط). انظر المثال التالي حيث تقيَّم (if (i % 2 == 0 إلى true إن كان العدد زوجيًا، وتذهب continue فورًا إلى بداية الحلقة، لكن لا يُنتَقَل إلى التعليمة التالية إن لم تُنفَّذ. for (int i = 0; i < 6; i++) { if (i % 2 == 0) continue; std::cout << i << " is an odd number\n"; } تكون النتيجة ما يلي: 1 is an odd number 3 is an odd number 5 is an odd number لا تُستخدم break و continue إلا نادرًا، ذلك أنه يصعب معهما قراءة الشيفرة وفهمها، وتُستخدم أساليب أخرى أبسط بدلًا منهما.فمثلًا يمكن إعادة كتابة الحلقة for الأولى التي تستخدم break على النّحو التالي: for (int i = 0; i < 4; i++) { std::cout << i << '\n'; } وبالمثل، يمكن إعادة كتابة المثال الثّاني الذي يحتوي continue كالتالي: for (int i = 0; i < 6; i++) { if (i % 2 != 0) { std::cout << i << " is an odd number\n"; } } التصريح عن المتغيرات في العبارات الشَّرطية يسمح بالتصريح عن كائن في شرط حلقات for أو while، وسيُدرَج ذلك الكائن في النّطاق حتى نهاية الحلقة، وسيكون متاحًا خلال كل تكرارات الحلقة: for (int i = 0; i < 5; ++i) { do_something(i); } // لم يعد في النطاق i for (auto& a : some_container) { a.do_something(); } // لم يعد في النطاق a while(std::shared_ptr<Object> p = get_object()) { p-> do_something(); } // لم يعد في النطاق p لا يمكنك فعل الشيء نفسه مع حلقة do...while؛ إذ عليك التصريح عن المتغير قبل الحلقة، ثمّ وضع المتغير والحلقة داخل نطاق محلّي (local scope) إن أردت أن يُحذَف المتغير بعد انتهاء الحلقة: // هذه الشّيفرة لن تُصرّف do { s = do_something(); } while (short s > 0); // جيّد short s; do { s = do_something(); } while (s > 0); وذلك لأنّ متن الحلقة do...while يقيَّم قبل الوصول إلى الجزء (while)، وعليه فإن التصاريح الموضوعة في ذلك الجزء لن تكون مرئيّة أثناء التّكرار الأول للحلقة. تكرار حلقة for على نطاق فرعي تستطيع التكرار على جزء فرعي من حاوية أو نطاق ما باستخدام الحلقات النطاقية (range-base loops)، وذلك من خلال إنشاء كائن وكيل (proxy object). template < class Iterator, class Sentinel=Iterator > struct range_t { Iterator b; Sentinel e; Iterator begin() const { return b; } Sentinel end() const { return e; } bool empty() const { return begin() == end(); } range_t without_front(std::size_t count = 1) const { if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) { count = (std::min)(std::size_t(std::distance(b, e)), count); } return { std::next(b, count), e }; } range_t without_back(std::size_t count = 1) const { if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) { count = (std::min)(std::size_t(std::distance(b, e)), count); } return { b, std::prev(e, count) }; } }; template < class Iterator, class Sentinel > range_t<Iterator, Sentinel> range( Iterator b, Sentinal e ) { return { b, e }; } template < class Iterable > auto range(Iterable & r) { using std::begin; using std::end; return range(begin(r), end(r)); } template < class C > auto except_first(C & c) { auto r = range(c); if (r.empty()) return r; return r.without_front(); } نستطيع الآن فعل ما يلي: std::vector < int > v = {1, 2, 3, 4}; for (auto i: except_first(v)) std::cout << i << '\n'; يكون الناتج: 2 3 4 يجب أن تتذكّر أنّ الكائنات الوسيطة (intermediate objects) المُنشأة في الجزء for(:range_expression)‎ من حلقة for ستُحذف عند بدء تنفيذ الحلقة. هذ الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 11: Loops‎ من الكتاب C++ Notes for Professionals
  10. المكررات (Iterators) وسيلة لتنفيذ عمليات على سلسلة عناصر على التوالي، رغم أنها ليست عناصر في نفسها، وإنما هي في الأساس عبارة عن مواضع (Positions)، كذلك فإنها امتداد لمفهوم المؤشرات، انظر المثال التالي: A B C يحتوي التسلسل السابق على ثلاثة عناصر، وأربعة مواضع كما يوضح التمثيل التالي: +---+---+---+---+ | A | B | C | | +---+---+---+---+ العناصر هي أشياء تؤلف تسلسلًا معيّنًا، أما المواضع فهي الأماكن التي يمكن أن تحدث فيها عمليات ما على ذلك التسلسل، فعند إدراج عنصر جديد فإننا ندرجه في موضع ما قبل أو بعد العنصر A مثلًا، وليس داخل العنصر A نفسه، بل حتى حذف العنصر يتم بإيجاد موضعه أولًا ثم حذفه (erase(A. من المكرِّرات إلى القيم للانتقال من موضع إلى قيمة، يجب أن يُحصّل (dereferenced) المُكرِّر: auto my_iterator = my_vector.begin(); // موضع auto my_value = *my_iterator; // قيمة يمكن النظر إلى المُكرِّر كمُحصِّلٍ للقيمة التي يشير إليها في التسلسل، لهذا يجب ألا تحاول أبدًا تحصيل الموضع end()‎ في التسلسل: +---+---+---+---+ | A | B | C | | +---+---+---+---+ ↑ ↑ | +-- مكرّر لا يشير إلى أي قيمة، لا تحاول تحصيله +--------------- A تحصيل هذا المكرّر سيعيد القيمة سيعيد التعبير begin() مكرِّرًا يشير إلى الموضع الأول، وذلك في جميع التسلسلات والحاويات (Containers) الموجودة في مكتبة C++ القياسية، بينما سيعيد end() مكرِّرًا يشير إلى موضع يلي الموضع الأخير (وليس الموضع الأخير نفسه)، لهذا تسمى تلك المكررات أحيانًا first و last. انظر المثال التالي: +---+---+---+---+ | A | B | C | | +---+---+---+---+ ↑ ↑ | | +- first +- last من الممكن الحصول على مُكرِّر لأي تسلسل، ذلك أن جميع التسلسلات -حتى الفارغة منها- تحتوي على موضع واحد على الأقل: +---+ | | +---+ في التسلسل الفارغ، يمثّل كل من begin()‎ و end()‎ نفس الموضع، ولا يمكن تحصيل أيّ منهما، انظر المثال التالي: +---+ | | +-----+ ↑ | +- empty_sequence.begin() | +- empty_sequence.end() كذلك يمكن تصوّر المكرِّرات على أنها إشارات تحدد المواضع بين العناصر: +---+---+---+ | A | B | C | +---+---+---+ ↑ ^ ^ ↑ | | +- first +- last ووفقًا لهذا التصوّر فإنّ تحصيل المُكرِّر سيعيد مرجعًا إلى العنصر الذي يأتي بعد المُكرِّر، وهذا مفيد في العمليات التالية: عمليات insert ستدرج عنصرًا في الموضع الذي يشير إليه المكرر. عمليات erase ستعيد مكررًا يتوافق مع نفس الموضع الذي تم المرور عليه. يتواجد المُكرّر، ومُكرّره المعكوس (Reverse Iterator) في نفس الموضع بين العناصر. المكرِّرات غير الصالحة (Invalid Iterators) يصير المكرر غيرَ صالحٍ إن لم يعد موضعُه جزءًا من التسلسل (مثال: أثناء عملية ما)، ولا يمكن تحصيل المكرر غير الصالح إلا بعد إسناده (Assign) إلى موضع آخر صالح. انظر المثال التالي الذي يكون فيه first صالحًا، ثم يخرج foo عن النطاق ويحذف، فيصير first بعد ذلك غير صالح. std::vector<int>::iterator first; { std::vector<int> foo; first = foo.begin(); } الخوارزميات ودوال توابع التسلسلات (sequence member functions) التي في مكتبة C++‎ القياسية لديها قواعد تضبط التعامل مع المكررات حين يتم إبطالها (invalidated)، ولكل خوارزمية طريقة مختلفة في التعامل مع المكررات وإبطالها. التنقل باستخدام المكررات تُستخدم المكررات للتنقل بين عناصر التسلسل إذ ينقل المكرر موضعه خلال التسلسل، ويستطيع أن يتحرك في كلا الاتجاهين، للأمام أو للخلف. auto first = my_vector.begin(); // تقديم المكرر بمَوضع واحد ++first; // تقديم المكرَر بموضع واحد std::advance(first, 1); // إعادة المكرَر إلى العنصر التالي first = std::next(first); // تأخير المكرَر بموضع واحد std::advance(first, -1); // إعادة المكرَر 20 موضعا إلى الأمام first = std::next(first, 20); // إعادة المكرر 5 مواضع إلى الخلف first = std::prev(first, 5); // إعادة المسافة بين مكرّرين auto dist = std::distance(my_vector.begin(), first); يجب أن يكون الوصول من الوسيط الأول إلى الوسيط الثاني لـ std::distance ممكنًا، بمعنى أن يكون first أصغر من أو يساوي second. ورغم أنك تستطيع إجراء عمليات حسابية على المكررات إلا أن هذا لا يعني أن جميع العمليات قابلة للتطبيق على المكررات كلها، فقد تصلح العملية a = b + 3;‎ لمكررات الوصول العشوائي (Random Access Iterators)، لكنها لن تصلح مع المكررات الأمامية (Forward Iterators) أو ثنائية الاتجاه (Bidirectional) التي يمكن نقلها ثلاثة مواضع للأمام عبر التعليمة b = a; ++b; ++b; ++b;‎، لذلك يوصى باستخدام دوال خاصة إن لم تكن تعرف نوع المكرِّر، كما في حالة قوالب الدوال التي تقبل المكررات. مفاهيم المُكرِّرات (Iterator Concepts) يصف معيار C++‎ العديد من مفاهيم المكررات، وقد جُمعت معًا وفقًا لسلوكها في التسلسلات التي تشير إليها، فإن عرفت السلوك العام للمكرِّر، عرفتَ سلوكه في أي تسلسل يشير إليه. وتوصف مفاهيم المكررات تلك مرتبة من أكثرهًا تقييدًا إلى أقلها: مكررات الدخل (Input Iterators): يمكن تحصيلها مرة واحدة فقط لكل موضع، ويمكن نقلها للأمام موضعًا واحدًا فقط في كل مرة، إذ لا تنتقل للوراء. مكررات التقدم (Forward Iterators): يمكن تحصيل هذه المكرّرات أيّ عدد من المرات. المكرّرات ثنائية الاتجاه (Bidirectional Iterators): هي مكررات تقدم تستطيع التحرك للخلف كذلك موضعًا واحدًا في كل مرة. المكرّرات العشوائية (Random Access Iterators): هي مكرّرات ثنائية الاتجاه تتحرك بحرية للأمام أو الخلف أي عدد من المواضع في كل مرة. المكرّرات المتجاورة (Contiguous Iterators) (منذ C++17): هي مكرّرات عشوائية تضمن أنّ البيانات التي تشير إليها متجاورة في الذاكرة. قد تختلف الخوارزميات وفقًا لمفاهيم المكرِّرات، فرغم إمكانية تقديم random_shuffle مثلًا للمكرِّرات الأماميّة إلا أن هناك بديلًا يتطلب مكرِّرات عشوائية يمكن توفيره، وسيكون أفضل. سمات المُكرِّرات (Iterator traits) توفّر سمات المكرّرات واجهة موحّدة لخصائص المكرِّرات. إذ تسمح لك بجلب نوع القيمة value_type ونوع الفرق difference_type وكذلك المؤشر pointer والمرجع reference، إضافة إلى فئة المكرر iterator_category، انظر المثال التالي: template <class Iter> Iter find(Iter first, Iter last, typename std::iterator_traits<Iter>::value_type val) { while (first != last) { if (*first == val) return first; ++first; } return last; } يمكن استخدام فئة المُكرِّر iterator_category لتخصيص الخوارزميات: template <class BidirIt> void test(BidirIt a, std::bidirectional_iterator_tag) { std::cout << "Bidirectional iterator is used" << std::endl; } template <class ForwIt> void test(ForwIt a, std::forward_iterator_tag) { std::cout << "Forward iterator is used" << std::endl; } template <class Iter> void test(Iter a) { test(a, typename std::iterator_traits<Iter>::iterator_category()); } فئات المكرِّرات هي في الأساس مفاهيم مُكرّرات، باستثناء أنّ المكرّرات المتجاورة (Contiguous Iterators) ليس لها وسم (tag) خاص بها، إذ أنها مُخصّصة لإيقاف الشيفرة (break code). المُكرِّر المُتّجِهي (Vector Iterator) تعيد begin مكرّرًا iterator يشير إلى العنصر الأول في حاوية التسلسل (sequence container)، بينما تعيد end مكرّرًا يشير إلى العنصر الأول بعد النهاية. إذا كان الكائن المتّجهي (vector object) ثابتًا const فإنّ كلًّا من begin و end ستعيدان مُكّررًا ثابتًا const_iterator، وإن أردت أن يُعاد مكرّر ثابت حتى لو لم تكن المتجهة ثابتة، فاستخدم cbegin و cend. انظر المثال التالي حيث نهيئ متجهًا باستخدام initializer_list: #include <vector> #include <iostream> int main() { std::vector<int> v = {1, 2, 3, 4, 5}; for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) { std::cout << *it << " "; } return 0; } سيكون الخرج: 1 2 3 4 5 مكرّرات القواميس map_iterator مكرّرات القواميس هي مكرّرات تشير إلى العنصر الأول في الحاوية. ستعيد الدالة مكررًا ثابتًا const_iterator إذا كان كائن الحاوية مؤهلًا ثبوتيًا (const_qualified)، أما إن كان خلاف ذلك فستعيد مكرّرًا عاديًا. انظر المثال التالي حيث ننشئ قاموسًا وندرج فيه بعض العناصر، ثم نكرره على جميع الأزواج: std::map<char, int> mymap; mymap['b'] = 100; mymap['a'] = 200; mymap['c'] = 300; for (std::map<char, int>::iterator it = mymap.begin(); it != mymap.end(); ++it) std::cout << it->first << " => " << it->second << '\n'; سيكون الخرج: a => 200 b => 100 c => 300 المكرِّرات العكسيّة (Reverse Iterators) تُستخدم المكررات العكسية reverse_iterator عندما نريد أن نحرِّك المكرِّر للخلف خلال قائمة أو متجه، ونحصل على تلك المكررات من مكرر ثنائي الاتجاه أو مكرر عشوائي، وهما يحتفظان بذلك المكرر كعضو يمكن الوصول إليه من خلال التابع base()‎. ولكي يتحرّك المُكرِّر للخلف، عليك استخدام rbegin()‎ و rend()‎ كمكرّرين يشيران إلى نهاية وبداية المجموعة على الترتيب. انظر المثال التالي الذي نكرر فيه إلى الخلف لنطبع (54321): std::vector<int> v{1, 2, 3, 4, 5}; for (std::vector<int>::reverse_iterator it = v.rbegin(); it != v.rend(); ++it) { cout << *it; } كذلك يمكن تحويل مكرّر عكسي إلى مكرّر أمامي من خلال التابع base()‎، ويشير المكرّر العكسي إلى العنصر الذي يلي المكرّر base()‎، انظر المثال التالي حيث تكون assert(&*r == &*(i-1));‎ صحيحة إن كان r و (i-1) يقبلان التحصيل (Dereferencing) ولم يكونا مكرريْن وكيلين (Proxy Iterators). std::vector<int>::reverse_iterator r = v.rbegin(); std::vector<int>::iterator i = r.base(); assert(&*r == &*(i-1)); +----+---+---+---+---+---+---+ | | 1 | 2 | 3 | 4 | 5 | | +----+---+---+---+---+---+---+ ↑ ↑ ↑ ↑ | | | | rend() | rbegin() end() | rbegin().base() begin() rend().base() ستكون العلاقة أبسط إن عدنا إلى التصور السابق حيث تحدِّد المكرراتُ المواضع بين العناصر، انظر: +---+---+---+---+---+ | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+ ↑ ↑ | | | end() | rbegin() begin() rbegin().base() rend() rend().base() مُكرِّر التدفق istream_iterator مُكرِّرات التدفق مفيدة عندما نحتاج إلى قراءة تسلسل أو طباعة بيانات منسّقة (formatted) من الحاوية، لنضرب مثلًا ببرنامج يطبع أرقامًا صحيحة تفصل بين كل منها فاصلتين متتاليتين --، ولنفصله جزءًا جزءًا للتوضيح، فأول جزء يوضح تدفقًا للبيانات، يكون أي عدد من محارف المسافات البيضاء فيه مقبولًا. std::istringstream istr("1\t 2 3 4"); std::vector < int > v; نبني الآن مكررات تدفق وننسخ البيانات من التدفق إلى متجه: std::copy( ونستخدم مكررًا يقرأ بيانات التدفق كأعداد صحيحة: std::istream_iterator < int > (istr), ينتج الباني الافتراضي مكررًا يشير إلى نهاية التدفق: std::istream_iterator < int > (), std::back_inserter(v)); نطبع الآن محتوى المتجه الذي سيكون في مجرى الدخل القياسي كأعداد صحيحة مفصولة بفاصلتين متتاليتين (--): std::copy(v.begin(), v.end(), std::ostream_iterator < int > (std::cout, " -- ")); انظر الآن الشيفرة كاملة دون التعليقات التوضيحية: std::istringstream istr("1\t 2 3 4"); std::vector<int> v; std::copy( std::istream_iterator<int>(istr), std::istream_iterator<int>(), std::back_inserter(v)); std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " -- ")); ويكون الناتج المطبوع للبرنامج أعلاه هو التالي: 1 -- 2 -- 3 -- 4 -- مكرِّرات C (المؤشرات) انظر المثال التالي لشيفرة ستُنتج الأرقام من 1 إلى 5 بحيث تطبع كل رقم على سطر منفصل: const int array[] = {1, 2, 3, 4, 5}; #ifdef BEFORE_CPP11 const int *first = array; const int *afterLast = first + sizeof(array) / sizeof(array[0]); for (const int *i = first; i < afterLast; ++i) { std::cout << *i << std::endl; } #else for (auto i = std::begin(array); i != std::end(array); ++i) { std::cout << *i << std::endl; } #endif الناتج: 1 2 3 4 5 تحليل الشيفرة const int array[] = { 1, 2, 3, 4, 5 }; ينشئ السطر السابق مصفوفة أعداد صحيحة (integer) فيها خمس قيَم، تذكر أن مصفوفات لغة C هي مجرد مؤشرات إلى مواضع في الذاكرة حيث تُخزَّن قيم المصفوفة بشكل متجاور. const int* first = array; const int* afterLast = first + sizeof(array) / sizeof(array[0]); هذان السّطران ينشئان مؤشّريْن اثنين، وقد أعطينا للمؤشر الأول قيمة مؤشّر المصفوفة array الذي يمثّل عنوان العنصر الأول فيها. ويعيد العامل sizeof حجم المصفوفة بالبايت عند استخدامه على مصفوفة في لغة C، وعندما نقسمه على حجم عنصر من المصفوفة سنحصل على عدد العناصر في المصفوفة، ومن ثم نستخدم ذلك لإيجاد عنوان الكتلة التالية للمصفوفة في الذاكرة. for (const int* i = first; i < afterLast; ++i) { في السّطر السابق أنشأنا مؤشرًا لاستخدامه كمكرّر، وقد هيّأناه عند عنوان العنصر الأول الذي نريد أن نبدأ التكرار منه، وسنستمر في التكرار ما دام i أصغر من afterLast، أي طالما يشير i إلى عنوانٍ داخل المصفوفة array. std::cout << *i << std::endl; أخيرًا، يمكننا الوصول إلى القيمة التي يشير إليها المُكرِّر i من خلال تحصيله، وذلك داخل الحلقة التكرارية. وهنا يعيد عاملُ التحصيل‏‏ * القيمةَ المخزّنة في العنوان i. اكتب مُكرِّرك الخاص يسود في لغات البرمجة الأخرى غير C++ أن توجد دالة تنتج تدفقًا (stream) من الكائنات مع استخدام حلقات تكرارية للمرور على عناصر ذلك التدفق، ويمكن تنفيذ ذلك في C++ أيضًا، انظر المثال التالي حيث نفصل عملية كتابة مكرر خاص بنا: template < class T > struct generator_iterator { using difference_type = std::ptrdiff_t; using value_type = T; using pointer = T * ; using reference = T; using iterator_category = std::input_iterator_tag; std::optional < T > state; std:: function < std::optional < T > () > operation; والآن سنخزن العنصر الحالي -إن كان موجودًا- في state: T operator * () const { return *state; } نكمل الآن بأن نستدعي العملية، ونكون قد وصلنا للنهاية إن أعادت nullopt: generator_iterator & operator++() { state = operation(); return *this; } generator_iterator operator++(int) { auto r = * this; ++( * this); return r; } لا يتساوى مكرران مدعومان بالمولِّدات (Generator Iterators) إلا إن كانا في حالة end. friend bool operator == (generator_iterator const & lhs, generator_iterator const & rhs) { if (!lhs.state && !rhs.state) return true; return false; } friend bool operator != (generator_iterator const & lhs, generator_iterator const & rhs) { return !(lhs == rhs); } سننشئ دالة بشكل مباشر من std::function مع إعطائها البصمة المناسبة. generator_iterator( std::function< std::optional() > f ):operation(std::move(f)) { if (operation) state = operation(); } نهيئ كل دوال التوابع الخاصة إلى القيمة default. generator_iterator( generator_iterator && ) =default; generator_iterator( generator_iterator const& ) =default; generator_iterator& operator=( generator_iterator && ) =default; generator_iterator& operator=( generator_iterator const& ) =default; generator_iterator() =default; }; يمكنك مشاهدة هذا المثال الحيّ. كذلك فإننا سنخزّن العنصر المُنشأ مبكرًا لمعرفة إن كنا وصلنا للنهاية أم لا، ونظرًا لعدم استخدام الدالة الخاصّة بمكرر المولِّد النهائي (end generator iterator)، فنستطيع إنشاء مجموعة من مكررات المولدات عن طريق نسخ std::function مرة واحدة فقط، ويكون مكرر المولد المنشأ افتراضيًا مساويًا لنفسه ولجميع مكررات المولدات النهائية. هذا الدرس جزء من سلسلة مقالات عن C++‎. ترجمة -بتصرّف- للفصل Chapter 9: Iterators من كتاب C++ Notes for Professionals
  11. المصفوفة (Array) هي مجموعة من عناصر تنتمي إلى نفس النوع، وموضوعة في أماكن متجاورة في الذاكرة، ويمكن الرجوع إلى كل عنصر من عناصر المصفوفة على حدة عبر مُعرِّف فريد يُسمَّى الفهرس. ويسمح ذلك بالتصريح عن قيم متعددة لمتغير ما ومن ثم الوصول إلى كل واحدة منها بشكل منفرد دون الحاجة إلى التصريح عن متغير لكل قيمة. تهيئة المصفوفة المصفوفة هي كتلة مكوّنة من مواقع متسلسلة في الذاكرة والمخصصة لنوع معيّن من المتغيرات. تخصيص المصفوفات يشبه تخصيص المتغيرات العادية، ولكن مع إضافة قوسين مربّعين إلى اسمها [] يحتويان على عدد يمثل عدد العناصر التي يمكن أن تحتويها ذاكرة المصفوفة. يستخدم المثال التالي مصفوفة تستخدم نوع int، واسم المتغير arrayOfInts، وعدد عناصرها هو [ 5 ]: int arrayOfInts[5]; يمكن التصريح عن المصفوفة وتهيئتها في نفس الوقت على النحو التالي: int arrayOfInts[5] = {10, 20, 30, 40, 50}; إذا هيّأت مصفوفة بسرد جميع عناصرها فلا يلزمك تضمين عدد عناصر المصفوفة داخل القوسين المعقوفين، إذ سيحسُبه المُصرّف تلقائيًا. في المثال التالي، تعداد المصفوفة هو 5: int arrayOfInts[] = {10, 20, 30, 40, 50}; كذلك نستطيع تهيئة العناصر الأولى فقط، مع تخصيص مساحة للمزيد من العناصر، وفي هذه الحالة يلزمك كتابة طول المصفوفة بين القوسين المعقوفين. انظر الشيفرة التالية حيث نخصص مصفوفةً خماسية (تحتوي 5 عناصر) مع تهيئتها جزئيًّا، سيُهيّئ المصرّف بقية العناصر بالقيمة الافتراضية لنوع العنصر (في هذه الحالة، تلك القيمة هي 0). int arrayOfInts[5] = {10,20}; أي أن عناصر المصفوفة السابقة هي (10, 20, 0, 0, 0). كذلك يمكن تهيئة مصفوفات أنواع البيانات الأساسية الأخرى بالطريقة نفسها. انظر المثال التالي للتصريح عن مصفوفة وتخصيص مساحة ذاكرة لها دون تهيئتها: char arrayOfChars[5]; أو للتصريح عنها مع تهيئتها: char arrayOfChars[5] = { 'a', 'b', 'c', 'd', 'e' } ; double arrayOfDoubles[5] = {1.14159, 2.14159, 3.14159, 4.14159, 5.14159}; string arrayOfStrings[5] = { "C++", "is", "super", "duper", "great!"}; لاحظ أنه عند الوصول إلى عناصر المصفوفة فإن فهرس المصفوفة (أو موضعها) يبدأ عند القيمة 0. انظر المثال التالي، حيث يكون العنصر 10 هو العنصر رقم 0، و20 هو العنصر رقم 1، وهكذا. int array[5] = { 10, 20, 30, 40, 50}; std::cout << array[4]; // 50 std::cout << array[0]; // 10 المصفوفات متعددة الأبعاد ذات الحجم الثابت يشير الفهرس m[y]‎ في المثال أدناه إلى الصف رقم yمن المصفوفة m، حيث y مؤشرٌ يبدأ من الصفر، ويمكن فهرسة هذا الصف على النحو التالي my ‎ والذي يشير إلى العنصر/العمود x من الصف y، وذلك يعني أن الفهرس الأخير هو الأسرع تغيرًا، ونطاقه في التصريح -حيث النطاق هنا هو رقم الأعمدة لكل صف- هو آخر و"أعمق" حجم محدَّد. #include <iostream> #include <iomanip> using namespace std; auto main() -> int { int const n_rows = 3; int const n_cols = 7; int const m[n_rows][n_cols] = { { 1, 2, 3, 4, 5, 6, 7 }, { 8, 9, 10, 11, 12, 13, 14 }, { 15, 16, 17, 18, 19, 20, 21 }, }; for( int y = 0; y < n_rows; ++y ) { for( int x = 0; x < n_cols; ++x ) { // m[y,x] لا تستخدم cout << setw( 4 ) << m[y][x]; } cout << '\n'; } } يكون الناتج: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 لا تدعم C++‎ صياغة خاصة لفهرسة المصفوفة متعددة الأبعاد، وإنما تتعامل معها على أنها مصفوفة مكونة من مصفوفات أخرى داخلها، وتستخدم الفهرسة العادية لكل مستوى. وبما أن C++‎ لا توفر دعمًا بشكل افتراضي للمصفوفات ذات الحجم المتغير أو المصفوفات الديناميكية (Dynamic Size Arrays) ما عدا التخصيص الديناميكي (Dynamic Allocation)، فإن المصفوفة الديناميكية تُستخدم غالبًا كصنف (Class). لكن هناك بعض التعقيدات المرتبطة بصياغة الفهرسة m[y][x]‎، إما بكشف الاستخدام ليستحيل عرض منقول المصفوفة مثلًا (Transposed Matrix) أو بإضافة حمل زائد على البرنامج عند تنفيذه بإعادة كائن وكيل (Proxy Object) من عامل الفهرسة operator[OD1][]‎، وعليه قد تكون صياغة الفهرسة مختلفة سواء في الشكل أو في ترتيب الفهارس، كما هو الحال في m(x,y)‎ أو m.at(x,y)‎ أو m.item(x,y)‎. المصفوفات الديناميكية (Dynamically sized raw array) انظر المثال التالي كمثال على مصفوفة ديناميكية، واعلم أن الأفضل عمومًا هو استخدام std::vector: #include <algorithm> // std::sort #include <iostream> using namespace std; auto int_from( istream& in ) -> int { int x; in >> x; return x; } auto main() -> int { cout << "Sorting n integers provided by you.\\n"; cout << "n? "; int const n = int_from( cin ); // n تخصيص مصفوفة عدد عناصرها int* a = new int[n]; for( int i = 1; i <= n; ++i ) { cout << "The #" << i << " number, please: "; a[i-1] = int_from( cin ); } sort( a, a + n ); for( int i = 0; i < n; ++i ) { cout << a[i] << ' '; } cout << '\\n'; delete[] a; } في بعض المُصرِّفات التي تدعم المصفوفات ذات الطول المتغير وفق معيار C99 ‏(variadic length arrays أو VLAs) كإضافة للّغة، يمكن تصريف برنامج يعلن عن مصفوفة T a[n];‎، حيث لا تُحدَّد n حتى وقت التشغيل (run-time). لكنّ C++‎ القياسية لا تدعم تلك المصفوفات، لذا يمكن استخدام تعبير new[]‎ لتخصيص مصفوفة ديناميكية بشكل يدوي، انظر المثال التالي حيث نخصص مصفوفة مكونة من n عنصر: int* a = new int[n]; تستطيع إلغاء تخصيص المصفوفة بعد استخدامها عبر delete[]‎: delete[] a; قيم المصفوفة التي صرّحنا عنها أعلاه غير محددة، ولكن يمكن تهيئة عناصرها عند القيمة صفر عبر إضافة قوسين فارغين () هكذا: new int[n]()‎، وعمومًا فإنه لأي نوع من أنواع البيانات، يهيئ هذا التعبير قيمَ المصفوفة بالقيمة الافتراضية لذلك النوع. لن تكون هذه الشيفرة آمنة للتنفيذ كجزء من دالة في أسفل هرمية الاستدعاء، ذلك أنه قد يحدث تسريب للذاكرة في حالة وجود استثناء قبل تعبير delete[]‎‎، (أو بعد ‎new[]‎)، وحل ذلك يكون بأتمتة عملية التنظيف عبر استخدام المؤشر الذكي std::unique_ptr، رغم أن الأفضل هو استخدام متجه (std::vector) إذ تلك هي وظيفته الأساسية. حجم المصفوفة انظر المثال التالي: #include // size_t, ptrdiff_t //-----------------------------------: using Size = ptrdiff_t; template < class Item, size_t n > constexpr auto n_items(Item( & )[n]) noexcept -> Size { return n; } //----------------------------------- الاستخدام: #include using namespace std; auto main() -> int { int const a[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4}; Size const n = n_items( a ); Size const n = n_items(a); int b[n] = {}; // a مصفوفة لها نفس حجم (void) b; cout <} يمكن الحصول على حجم المصفوفة في لغة C عبر: sizeof(a)‎ أو sizeof(a[0])‎، في حال تمرير مؤشر كوسيط، لكن تكون النتيجة بشكل عام غير دقيقة. أما في C++11، يمكنك الحصول على حجم المصفوفة بالتعبير التالي: std::extent<decltype(MyArray)>::value; مثال: char MyArray[] = { 'X','o','c','e' }; const auto n = std::extent<decltype(MyArray)>::value; std::cout << n << "\n"; // يطبع 4 لم يكن في أي إصدار من C++‎ إلى الإصدار C++17 أي آلية أو مكتبة قياسية مضمَّنة فيها للحصول على أحجام المصفوفات، وإنما نحصل على ذلك بتمرير مرجع يشير للمصفوفة إلى قالب دالة (function template) كما هو موضح أعلاه. معامل حجم القالب (template size parameter) هو size_t، وهو غير متوافق مع النوع المؤشَّر (signed type) للقيمة المعادة من الدّالة Size، وذلك لأجل التوافق مع المُصرّف g++‎ الذي يصر أحيانًاعلى size_t من أجل مطابقة القوالب. كبديل عن ذلك، يمكن استخدام std::size في C++17 والإصدارات اللاحقة لها، إذ هو تابع مخصص للمصفوفات. توسيع المصفوفات الديناميكية باستخدام المتجهات انظر المثال التالي الذي يوضح استخدام متجه std::vector كمصفوفة ديناميكية قابلة للتوسع: #include <algorithm> // std::sort #include <iostream> #include <vector> // std::vector using namespace std; int int_from( std::istream& in ) { int x = 0; in >> x; return x; } int main() { cout << "Sorting integers provided by you.\n"; cout << "You can indicate EOF via F6 in Windows or Ctrl+D in Unix-land.\n"; vector < int > a; // ← الحجم يساوي 0 افتراضيا while( cin ) { cout << "One number, please, or indicate EOF: "; int const x = int_from( cin ); if( !cin.fail() ) { a.push_back( x ); } // التوسيع بحسب الضرورة } sort( a.begin(), a.end() ); int const n = a.size(); for( int i = 0; i < n; ++i ) { cout << a[i] << ' '; } cout << '\n'; } std::vector هو قالب صنف في المكتبة القياسية التي توفر مفهوم المصفوفة ذات الحجم المتغير (الديناميكية)، وتتكفل تلك المكتبة بإدارة الذاكرة، كما أنّ المخزن المؤقت (buffer) متصل (contiguous)، لذا يمكن تمرير مؤشر يشير إلى المخزن المؤقت (على سبيل المثال ‎&v[0]‎ أو v.data()‎) إلى دوال الواجهة البرمجية (API) التي تتطلّب مصفوفةً خام (raw array). يمكن توسيع المتجهة vector في وقت التشغيل عبر التابع push_back الذي يضيف عنصرًا إلى المصفوفة. يُحدَّد تعقيد سلسلة عمليات push_back عددها n، بما في ذلك عمليات النسخ والنقل المستخدمتين في توسيعات المتجهات، في المتوسط من خلال O(n)، ويُنفّذ ذلك داخليًا عبر مضاعفة حجم المخزن المؤقّت للمتجهة vector وسِعتها عند الحاجة إلى حجم أكبر. فمثلًا، إذا كان المخزن المؤقّت يبدأ بالحجم 1، ثم يتضاعف حجمه بشكل متكرر حسب الحاجة، فإنّه في مقابل n=17 عملية استدعاءً للتابع push_back، ستكون هناك 1 + 2 + 4 + 8 + 16 = 31 عملية نسخ، أي أقل من 2 × n = 34. لكن بشكل عام فلا يمكن أن يتجاوز مجموع هذا التسلسل القيمة 2 × n. مقارنةً بمثال المصفوفة الديناميكيّة، فإنّ هذه الشيفرة المستندة إلى المتجهات لا تتطلب من المستخدم أن يحدد (أو يعرف) عدد العناصر مقدّمًا، وإنما تُوسَّع المتجهة حسب الضرورة بدلًا من ذلك. استخدام std::vector في المصفوفة الديناميكية للتخزين بدءًا من الإصدار C++14 لم يعد هناك أي صنف (Class) مخصص للمصفوفات الديناميكية في المكتبة القياسية، وإنما ستجد مثل تلك الأصناف التي تدعم الحجم المتغير في بعض مكتبات الطرف الثالث، بما في ذلك مكتبة Boost Matrix (مكتبة فرعية داخل مكتبة Boost). وإن لم ترد الاعتماد على Boost أو أيّ مكتبة أخرى، فيمكنك كتابة المصفوفات متعددة الأبعاد الديناميكية في ++C على نحو ما في المثال التالي، حيث vector هي متجهة من النوع std::vector. vector<vector<int>> m( 3, vector<int>( 7 ) ); تُنشأ المصفوفة هنا عن طريق نسخ متجه صفِّي (row vector) عددًا من المرات قدره n مرة، حيث n هو عدد الصفوف الذي يساوي 3 في مثالنا أعلاه. تمتاز تلك الطريقة بدعمها لصيغة الفهرسة m[y][x]‎ كما في المصفوفة متعددة الأبعاد ثابتة الحجم، لكنها غير فعّالة من جهة أخرى إذ تتطلّب تخصيص ذاكرة ديناميكيّ لكل صفّ، كما أنّها غير آمنة بسبب إمكانية تغيير حجم الصف عن غير عمد. وعلى أي حال توجد طريقة أخرى أفضل وهي استخدام متجه واحد كتخزين للمصفوفة، وتوجيه شيفرة العميل (x,y) إلى فهرس مناسب في ذلك المتجه. انظر المثال التالي لمصفوفة متغير الحجم (ديناميكية) تستخدم std::vector للتخزين: //--------------------------------------------- الألية: #include // std::copy #include // assert #include // std::initializer_list #include // std::vector #include // ptrdiff_t namespace my { using Size = ptrdiff_t; using std::initializer_list; using std::vector; template <class Item> class Matrix { private: vector items_; Size n_cols_; auto index_for(Size const x, Size const y) const -> Size { return y * n_cols_ + x; } public: auto n_rows() const -> Size { return items_.size() / n_cols_; } auto n_cols() const -> Size { return n_cols_; } auto item(Size const x, Size const y) -> Item & { return items_[index_for(x, y)]; } auto item(Size const x, Size const y) const -> Item const & { return items_[index_for(x, y)]; } Matrix() : n_cols_(0) {} Matrix(Size const n_cols, Size const n_rows) : items_(n_cols * n_rows), n_cols_(n_cols) { } Matrix(initializer_list<initializer_list> const &values) : items_(), n_cols_(values.size() == 0 ? 0 : values.begin()->size()) { for (auto const &row : values) { assert(Size(row.size()) == n_cols_); items_.insert(items_.end(), row.begin(), row.end()); } } }; } // namespace my //--------------------------------------------- الاستخدام: using my::Matrix; auto some_matrix() -> Matrix { return { {1, 2, 3, 4, 5, 6, 7}, {8, 9, 10, 11, 12, 13, 14}, {15, 16, 17, 18, 19, 20, 21}}; } #include #include using namespace std; auto main() -> int { Matrix const m = some_matrix(); assert(m.n_cols() == 7); assert(m.n_rows() == 3); for (int y = 0, y_end = m.n_rows(); y < y_end; ++y) { for (int x = 0, x_end = m.n_cols(); x < x_end; ++x) { cout <← Note : not `m[y][x]`! } cout < } } يكون الخرج: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 الشيفرة أعلاه ليست مناسبة لا توافق معايير بيئات الإنتاج في الشركات، وإنما صُممت لتوضيح المبادئ الأساسية وخدمة احتياجات الطلاب الذي يتعلمون C++‎، فمثلًا يمكن تحديد التحميل الزائد لـ ()operator لتبسيط صيغة الفهرسة. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 8: Arrays من كتاب C++ Notes for Professionals
  12. تستعرض هذه المقالة أفضل الممارسات المُتعارف عليها لكتابة شيفرات SQL نظيفة، وكذلك تأمين الشيفرات عبر التحوّط من هجمات حقن SQL. الشيفرات البرمجية النظيفة في SQL هذه بعض النصائح والقواعد حول كيفية كتابة استعلامات SQL تراعي أفضل الممارسات، وذات مقروئية عالية. تنسيق وتهجئة الكلمات المفتاحية والأسماء أسماء الجداول والأعمدة هناك طريقتان شائعتان لكتابة أسماء الجداول والأعمدة، وهما ‎CamelCase‎ و ‎snake_case‎ كما يوضح المثال التالي: SELECT FirstName, LastName FROM Employees WHERE Salary > 500; SELECT first_name, last_name FROM employees WHERE salary > 500; يجب أن تعطي الأسماء فكرة عمّا هو مُخزّن في الكائن المُسمّى. هناك نقاش محتدم حول ما إذا كان الأفضل أن تكون أسماء الجداول بصيغة المفرد أو الجمع، ولكنّ الشائع استخدام صيغة الجمع. تُنقِص إضافة سابقات أو لاحقات، مثل ‎tbl‎ أو ‎col‎، إلى الأسماء مقروئية الشيفرة، لذلك يُفضل تجنبها. إلا أنّها قد تكون ضرورية في بعض الأحيان لتجنّب التداخل مع الكلمات المفتاحية في SQL، وغالبًا ما تُستخدم مع الزنادات (triggers) والفهارس (والتي لا تُذكر أسماؤها في الاستعلامات عادةً). الكلمات المفتاحية الكلمات المفتاحية في SQL ليست حسّاسة لحالة الأحرف. ولكن تغلُب كتابتها بأحرف كبيرة. المسافات البادئة Indenting لا يوجد معيار مقبول ومُوحّد للمسافات البادئة. لكنّ الجميع يتفق على أنّ حشر كل شيء في سطر واحد أمر سيء مثل: SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC; أضعف الإيمان أن تضع كل عبارة في سطر جديد، مع تقسيم السطور الطويلة: SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC; في بعض الأحيان، تُوضع نفس المسافة البادئة قبل الأسطر التي تعقُب الكلمات المفتاحية في SQL: SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC; (يمكن القيام بذلك أيضًا عند محاذاة الكلمات المفتاحية في SQL إلى اليمين.) هناك طريقة شائعة أخرى، وهي وضع الكلمات المفتاحية المهمّة في سطور خاصّة على النحو التالي: SELECT d.Name, COUNT(*) AS Employees FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.Name != 'HR' HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC; تُحسّن المحاذاة الرأسية للعبارات المتماثلة مقروئية الشيفرة: SELECT Model, EmployeeID FROM Cars WHERE CustomerID = 42 AND Status = 'READY'; استخدام عدّة أسطر يصعّب تضمين أوامر SQL في لغات البرمجة الأخرى. إلا أنّ العديد من اللغات لديها آلية للتعامل مع السلاسل النصية متعددة الأسطر، مثل ‎@"..."‎ في C#‎‎ أو ‎"""..."""‎ في Python أو ‎R"(...)"‎ في C++‎‎‎. SELECT *‎‎ تعيد العبارة ‎SELECT *‎ جميع الأعمدة بنفس ترتيب ظهورها في الجدول. عند استخدام ‎SELECT *‎، فقد تتغيّر البيانات المُعادة من الاستعلام كلّما تغيّر تعريف الجدول. وهذا يضعف توافقية الإصدارات المختلفة من التطبيق أو قاعدة البيانات مع بعضها بعضًا. علاوة على ذلك، فإنّ قراءة الأعمدة غير الضرورية قد يرفع من مساحة القرص المُستخدَمة، والدخل / الخرج الشبكي (network I/O). لذا عليك دائمًا تحديد العمود (أو الأعمدة) الذي تريد استردادها صراحة: SELECT * -- تجنّب هذا SELECT ID, FName, LName, PhoneNumber -- هذا أفضل FROM Emplopees; (لا تنطبق هذه الاعتبارات عند إجراء استعلامات تفاعلية - interactive queries.) بالمقابل، ليس هناك ضرر من استخدام ‎SELECT *‎ في استعلام فرعي لعبارة EXISTS، ذلك أنّ EXISTS تتجاهل البيانات الفعلية على أيّ حال (إذ تكتفي بالتحقق من أنّه تمّ العثور على صفّ واحد على الأقل). للسبب نفسه، لا فائدة من إدراج أيّ عمود (أو أعمدة) معيّنة في عبارة EXISTS، لذلك يُفضّل استخدام ‎SELECT *‎: -- سرد الأقسام التي لم يُعيّن فيها أيّ موظف حديثا SELECT ID, Name FROM Departments WHERE NOT EXISTS (SELECT * FROM Employees WHERE DepartmentID = Departments.ID AND HireDate >= '2015-01-01'); عمليات الضمّ Joins يجب دائمًا استخدام عمليات الضمّ الصريحة (Explicit joins)؛ لأنّ عمليات الضمّ الضمنية (implicit joins) تطرح العديد من المشاكل، مثلًا: في عمليات الضمّ الضمنية، يكون شرط الضمّ داخل عبارة WHERE مخلوطًا مع شروط أخرى. وذلك يصعّب معرفة الجداول المضمومة، وكيفية ضمّها. بسبب النقطة أعلاه، يتعاظم خطر حدوث أخطاء. في SQL القياسية، عمليات الضمّ الصريحة هي الطريقة الوحيدة لاستخدام الضمّ الخارجي: SELECT d.Name, e.Fname || e.LName AS EmpName FROM Departments AS d LEFT JOIN Employees AS e ON d.ID = e.DepartmentID; يتيح الضمّ الصريح استخدام عبارة USING كما يوضّح المثال التالي: SELECT RecipeID, Recipes.Name, COUNT(*) AS NumberOfIngredients FROM Recipes LEFT JOIN Ingredients USING (RecipeID); (يتطلب هذا أن يستخدم كلا الجدولين اسم العمود نفسه. تزيل USING تلقائيًا العمود المكرّر من النتيجة، وهكذا سيُعيد الضم في الاستعلام أعلاه عمودًا ‎RecipeID‎ واحدا.) حقن SQL حقن SQL هي تقنية يستخدمها القراصنة للوصول إلى جداول قاعدة بيانات موقع معيّن عن طريق حقن تعليمات SQL في حقل إدخال. إذا لم يكن خادم الويب مُجهّزا للتعامل مع هجمات حقن SQL، فيمكن للمخترقين خداع قاعدة البيانات، وجعلها تنفّذ شيفرة SQL إضافية. والتي قد تمكّنهم من ترقية صلاحيات حساباتهم، أو الوصول إلى المعلومات الشخصية لحساب آخر، أو إجراء أيّ تعديلات أخرى على قاعدة البيانات. لنفترض أنّ استدعاء معالج تسجيل الدخول إلى موقعك يبدو كما يلي: https://somepage.com/ajax/login.ashx?username=admin&password=123 الآن في login.ashx، ستقرأ القيم التالية: strUserName = getHttpsRequestParameterString("username"); strPassword = getHttpsRequestParameterString("password"); يمكنك استعلام قاعدة البيانات للتحقق ممّا إذا كان هناك مستخدم له كلمة المرور هذه. لذا ستنشئ استعلام SQL التالي: txtSQL = "SELECT * FROM Users WHERE username = '" + strUserName + "' AND password = '"+ strPassword +"'"; سيعمل هذا الاستعلام بلا مشاكل إذا لم يحتو اسم المستخدم وكلمة المرور على علامات اقتباس. ولكن إن احتوى أحد المعاملات على علامات اقتباس، فإنّ شيفرة SQL المُرسلة إلى قاعدة البيانات ستبدو كما يلي: -- strUserName = "d'Alambert"; txtSQL = "SELECT * FROM Users WHERE username = 'd'Alambert' AND password = '123'"; سينتج عن هذا خطأ في الصياغة، لأنّ علامة الاقتباس بعد ‎d‎ في ‎d'Alambert‎ تنتهي بشيفرة SQL. يمكنك تصحيح هذا عن طريق تهريب (escaping) علامات الاقتباس في اسم المستخدم وكلمة المرور على النحو التالي: strUserName = strUserName.Replace("'", "''"); strPassword = strPassword.Replace("'", "''"); هناك حلّ آخر أفضل، وهو استخدام المعاملات: cmd.CommandText = "SELECT * FROM Users WHERE username = @username AND password = @password"; cmd.Parameters.Add("@username", strUserName); cmd.Parameters.Add("@password", strPassword); إذا لم تستخدم المعاملات، ونسيت استبدال علامات الاقتباس ولو في قيمة واحدة، فيمكن للقرصان استخدام هذا لتنفيذ أوامر SQL في قاعدة البيانات الخاصة بك. على سبيل المثال، يمكن للقرصان أن يعيّن كلمة المرور التالية: lol'; DROP DATABASE master; -- وبعدها ستبدو SQL كالتالي: "SELECT * FROM Users WHERE username = 'somebody' AND password = 'lol'; DROP DATABASE master; --'"; لسوء الحظ، هذه شيفرة SQL صحيحة، وستنفّذها قاعدة البيانات DB! هذا النوع من الهجمات يسمّى حقن SQL. هناك أشياء أخرى كثيرة يمكن أن يقوم بها القرصان، مثل سرقة عناوين البريد الإلكتروني الخاصة بالمستخدمين، أو سرقة كلمات المرور خاصتهم، أو سرقة أرقام بطاقات الائتمان، أو سرقة أيّ نوع من البيانات في قاعدة البيانات. لهذا السبب، عليك دائمًا تهريب السلاسل النصية. لمّا كان النسيان طبيعة في الإنسان، ينصح الكثيرون باستخدام المعاملات دائمًا. لأنّ إطارات لغة البرمجة المُستخدمة تتكفّل بتهريبها نيابة عنك. مثال على حقن بسيط إذا تم إنشاء عبارة SQL على النحو التالي: SQL = "SELECT * FROM Users WHERE username = '" + user + "' AND password ='" + pw + "'"; db.execute(SQL); سيكون بمقدور القرصان سرقة بياناتك عن طريق إعطاء كلمة مرور من هذا القبيل ‎pw' or '1'='1‎؛ وهكذا تصبح عبارة SQL الناتجة على النحو التالي: SELECT * FROM Users WHERE username = 'somebody' AND password ='pw' or '1'='1' العبارة ‎'1'='1'‎ صحيحة دائمًا، لذلك سيتم اختيار كل الصفوف. لمنع هذا، استخدم معاملات SQL على النحو التالي: SQL = "SELECT * FROM Users WHERE username = ? AND password = ?"; db.execute(SQL, [user, pw]); ترجمة -وبتصرّف- للفصلين من 61 إلى 62 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال السابق: تصميم الجداول ومعلومات المخطط وترتيب تنفيذ الاستعلامات في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  13. تستعرض هذه المقالة عددا من مواضيع SQL، مثل كيفية تصميم جداول قواعد البيانات، واستخدام المرادفات، وكيفية استخلاص المعلومات المتعلقة بقاعدة البيانات عبر معلومات المخطط، والترتيب الذي تُنفّذ وفقه عبارات واستعلامات SQL. تصميم الجداول Table Design لا تنحصر وظائف أنظمة قواعد البيانات العلائقية في عرض البيانات في الجداول، وكتابة عبارات SQL لسحب تلك البيانات. إن كان تصميم الجداول سيئًا، فقد يؤدي ذلك إلى إبطاء تنفيذ الاستعلامات، ويمكن أن يؤثر على عمل قاعدة البيانات، بحيث لا تعمل كما هو متوقع. لذا لا ينبغي النظر إلى جداول قاعدة البيانات كما لو كانت مجرد جداول عادية؛ إذ يتوجّب أن تتّبع مجموعة من القواعد حتى تكون علائقية حقًّا. هذه هي القواعد الخمسة التي ينبغي أن تتوفّر في أيّ جدول علائقي: أن تكون كل القيم ذرّية (atomic)، أي يجب أن تكون قيمة كل حقل من كل صفّ قيمة واحدة. يجب أن تنتمي بيانات كل حقل إلى نفس نوع البيانات. يجب أن يكون لكل حقل اسمًا فريدًا. يجب أن يحتوي كل صفّ في الجدول على قيمة واحدة على الأقل تجعله متفرّدًا عن السجلات الأخرى في الجدول. لا ينبغي أن يكون لترتيب الصفوف والأعمدة أيّ تأثير. هذا مثال على جدول يتوافق مع القواعد الخمس أعلاه: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } Id Name DOB Manager 1 Fred 11/02/1971 3 2 Fred 11/02/1971 3 3 Sue 08/07/1975 2 لنتحقق من القواعد السابقة: القاعدة 1: كل القيم ذرّية. إذ لا تحتوِي الحقول ‎Id‎ و ‎Name‎ و ‎DOB‎ و ‎Manager‎ إلّا قيمًا مُنفردة (single) فقط. القاعدة 2: لا يحتوي الحقل ‎Id‎ إلّا على الأعداد الصحيحة، فيما يحتوي الحقل ‎Name‎ حصرًا على القيم النصية (يمكننا إضافة أنها جميعًا تتألف من أربعة أحرف أو أقل)، فيما يحتوي الحقل ‎DOB‎ على تواريخ من نوع صالح، ويحتوي الحقل ‎Manager‎ على أعداد صحيحة (يمكننا إضافة أنّها تتوافق مع حقل المفاتيح الرئيسية في جدول المدراء managers). القاعدة 3: ‎Id‎ و ‎Name‎ و ‎DOB‎ و ‎Manager‎ هي أسماء عناوين فريدة للحقول داخل الجدول. القاعدة 4: يميّز الحقل ‎Id‎ كلّ السجلّات، ويجعل كلّ سجلّ مختلفًا عن السجلات الأخرى داخل الجدول. هذا مثال على جدول ذي تصميم سيء: Id Name DOB Name 1 Fred 11/02/1971 3 1 Fred 11/02/1971 3 3 Sue Friday the 18th July 1975 2, 1 لنتحقق من القواعد السابقة: القاعدة 1: يحتوي الحقل الثاني على قيمتين، 2 و 1. القاعدة 2: يحتوي الحقل DOB على نوعي بيانات مختلفين، نوع التاريخ، ونوع النصوص. القاعدة 3: هناك حقلان لهما الاسم نفسه ("name"). القاعدة 4: السجل الأول والثاني متماثلان تمامًا. القاعدة 5: هذه القاعدة مُستوفاة. المرادفات Synonyms المرادف (Synonym) هو كُنية أو اسم بديل لكائن في قاعدة بيانات، هذا الكائن قد يكون جدولًا أو معرضًا أو إجراءًا مُخزّنًا، أو سلسلة …إلخ. يوضّح المثال التالي كيفية إنشاء المرادفات: CREATE SYNONYM EmployeeData FOR MyDatabase.dbo.Employees مخطط المعلومات Information Schema مخطّط المعلومات (Information Schema) هو استعلام يوفّر معلومات مفيدة للمستخدمين النهائيين عن أنظمة إدارة قواعد البيانات (RDBMS). تتيح مثل هذه الاستعلامات للمستخدمين إمكانية العثور السريع على جداول قاعدة البيانات التي تحتوي أعمدة معيّنة، كما يحدث عندما ترغب في ربط البيانات من جدولين بشكل غير مباشر عبر جدول ثالث دون معرفة مُسبقة بالجداول التي قد تحتوي على مفاتيح أو أعمدة أخرى مشتركة مع الجداول المستهدفة. يستخدم المثال التالي تعبيرًا T-SQL، ويبحث عن مخطّط المعلومات الخاصّ بقاعدة البيانات: SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_NAME LIKE '%Institution%' تحتوي النتيجة على قائمة بالأعمدة المُطابقة، وأسماء جداولها، ومعلومات أخرى مفيدة. ترتيب التنفيذ Order of Execution تُنفّذ عبارات استعلامات SQL وفق ترتيب محدّد. تستعرض هذه الفقرة هذا الترتيب: /*(8)*/ SELECT /*9*/ DISTINCT /*11*/ TOP /*(1)*/ FROM /*(3)*/ JOIN /*(2)*/ ON /*(4)*/ WHERE /*(5)*/ GROUP BY /*(6)*/ WITH {CUBE | ROLLUP} /*(7)*/ HAVING /*(10)*/ ORDER BY /*(11)*/ LIMIT إليك الترتيب الذي تتم به معالجة الاستعلامات، مع وصف مختصر لكلّ منها (تشير VT إلى "Virtual Table" أي جدول وهمي، وتوضّح كيف يتم إنتاج مختلف البيانات أثناء معالجة الاستعلام): FROM: تنفّذ جداء ديكارتي (ضمّ متقاطع cross join) بين الجدولين الأولين في عبارة FROM، ونتيجة لذلك، يُنشأ جدول وهمي VT1 ON: ترشِّح الجدول الوهمي VT1. ولا تُدرج إلا الصفوف التي تعيد TRUE إلى الجدول الوهمي VT2. OUTER: في حال الضمّ الخارجي OUTER JOIN (على عكس الضمّ المتقاطع CROSS JOIN أو الضمّ الداخلي INNER JOIN)، تُضاف صفوف الجدول أو الجداول المحفوظة (preserved table) التي لم يُعثَر فيها على تطابق إلى صفوف الجدول الوهمي VT2 كصفوف خارجية، وينتُج عن ذلك الجدول VT3. في حال كان هناك أكثر من جدولين في عبارة FROM، تُطبَّق الخطوات من 1 إلى 3 بشكل متكرر بين نتيجة عملية الضمّ الأخيرة والجدول التالي في عبارة FROM إلى أن تُعالج جميع الجداول. WHERE: ترشِّح الجدول VT3. ولا تُدرج إلا الصفوف التي تعيد TRUE إلى الجدول VT4 GROUP BY: تُقسّم صفوف الجدول الوهمي VT4 إلى مجموعات بناءً على قائمة الأعمدة المحدّدة في عبارة GROUP BY. وينجم عن ذلك إنشاء جدول VT5. CUBE | ROLLUP: تُضاف مجموعات أجزاء - Supergroups - (مجموعات مؤلّفة من مجموعات) إلى صفوف VT5، وينتُج الجدول الوهمي VT6. HAVING: ترشِّح الجدول VT6. ولا تُدرج إلا المجموعات التي تعيد القيمة TRUE إلى الجدول VT7. SELECT: تُعالج قائمة SELECT، ويُنشأ الجدول VT8. DISTINCT: تُزال الصفوف المكرّرة من VT8. ويُنشأ الجدول VT9. ORDER BY: تُرتَّب صفوف الجدول VT9 وفقًا لقائمة الأعمدة المحدّدة في عبارة ORDER BY، كما يُنشأ مُؤشّر - cursor - ‏(VC10). TOP: يُختار العدد أو النّسبة المئوية المحدّدة من الصفوف من بداية الجدول VC10. ويُنشأ الجدول VT11 ثُم يُعاد إلى المُستدعي - caller - (العبارة LIMIT لها نفس وظيفة TOP في بعض لهجات SQL، مثل Postgres و Netezza.) ترجمة -وبتصرّف- للفصول من 57 إلى 60 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: تنظيم شيفرات SQL وتأمينها المقال السابق: الاستعلامات الفرعية والإجراءات في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  14. تستعرض هذه المقالة بعض المواضيع المتقدمة عن تنفيذ الشيفرات في SQL، مثل الاستعلامات الفرعية، وكتل التنفيذ، والإجراءات المُخزّنة، والزنادات، والمُعاملات. الاستعلامات الفرعية Subqueries الاستعلامات الفرعية هي استعلامات داخلية أو متشعّبة داخل استعلام آخر في SQL. يُمكن أن تُضمّن الاستعلامات الفرعية داخل ‎FROM‎ أو SELECT أو WHERE. الاستعلامات الفرعية في عبارة FROM تتصرّف الاستعلامات الفرعية في عبارة ‎FROM‎ بشكل مشابه للجداول المؤقتة المُنشأة أثناء تنفيذ استعلام، والمفقودة إثر ذلك. SELECT Managers.Id, Employees.Salary FROM ( SELECT Id FROM Employees WHERE ManagerId IS NULL ) AS Managers JOIN Employees ON Managers.Id = Employees.Id الاستعلامات الفرعية في عبارة SELECT إليك مثال على استعلام فرعي في SELECT: SELECT Id, FName, LName, (SELECT COUNT(*) FROM Cars WHERE Cars.CustomerId = Customers.Id) AS NumberOfCars FROM Customers الاستعلامات الفرعية في عبارة WHERE يمكنك استخدام استعلام فرعي لترشيح مجموعة النتائج. على سبيل المثال، تعيد الشيفرة التالية الموظفين الأعلى أجرًا فقط: SELECT * FROM Employees WHERE Salary = (SELECT MAX(Salary) FROM Employees) الاستعلامات الفرعية المرتبطة Correlated Subqueries الاستعلامات الفرعية المرتبطة (والمعروفة أيضًا باسم المتزامنة أو المتّسقة) هي استعلامات متشعّبة تحتوي مرجعًا يشير إلى الصفّ الحالي في الاستعلام الخارجي: SELECT EmployeeId FROM Employee AS eOuter WHERE Salary > ( SELECT AVG(Salary) FROM Employee eInner WHERE eInner.DepartmentId = eOuter.DepartmentId ) الاستعلام الفرعي ‎SELECT AVG(Salary) ...‎ مرتبط لأنّه يشير إلى الصفّ ‎Employee‎ من الجدول ‎eOuter‎ من الاستعلام الخارجي. ترشيح نتائج الاستعلام باستخدام استعلام مُنفَّذ على جدول آخر يختار الاستعلام التالي جميع الموظفين غير الموجودين في جدول المشرفين Supervisors: SELECT * FROM Employees WHERE EmployeeID not in (SELECT EmployeeID FROM Supervisors) يمكن تحقيق النتائج نفسها باستخدام الضم اليساري LEFT JOIN: SELECT * FROM Employees AS e LEFT JOIN Supervisors AS s ON s.EmployeeID=e.EmployeeID WHERE s.EmployeeID is NULL الاستعلامات الفرعية في عبارة FROM يمكنك استخدام الاستعلامات الفرعية لتعريف جدول مؤقّت واستخدامه في عبارة FROM الخاصّة بالاستعلام الخارجي. تبحث الشيفرة التالية عن المدن في جدول الطقس weather التي تتغيّر درجات الحرارة اليومية الخاصّة بها بأكثر من 20 درجة: SELECT * FROM (SELECT city, temp_hi - temp_lo AS temp_var FROM weather) AS w WHERE temp_var > 20; النتيجة: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } city temp_var ST LOUIS 21 LOS ANGELES 31 LOS ANGELES 23 LOS ANGELES 31 LOS ANGELES 27 LOS ANGELES 28 LOS ANGELES 28 LOS ANGELES 32 الاستعلامات الفرعية في عبارة WHERE يبحث المثال التالي عن المدن (من مثال المدن) التي يقل تعداد سكانها عن متوسط درجة الحرارة فيها (يتم الحصول عليها عن طريق استعلام فرعي): SELECT name, pop2000 FROM cities WHERE pop2000 < (SELECT avg(pop2000) FROM cities); في المثال أعلاه، يحدّد الاستعلام الفرعي SELECT avg(pop2000) FROM شرط عبارة WHERE. النتيجة: name pop2000 San Francisco 776733 ST LOUIS 348189 Kansas City 146866 كتل التنفيذ Execution blocks تُستخدم الكلمتان المفتاحيتان BEGIN و END لبدء كتلة تنفيذ (Execution block) وإنهائها على التوالي، كما يوضح المثال التالي: BEGIN UPDATE Employees SET PhoneNumber = '5551234567' WHERE Id = 1; UPDATE Employees SET Salary = 650 WHERE Id = 3; END الإجراءات المخزّنة Stored Procedures يمكن إنشاء الإجراءات المخزّنة عبر واجهة المستخدم الرسومية الخاصة ببرنامج إدارة قاعدة البيانات (مثال SQL Server)، أو من خلال عبارة SQL كما يلي: -- تحديد الاسم والمعاملات CREATE PROCEDURE Northwind.getEmployee @LastName nvarchar(50), @FirstName nvarchar(50) AS -- تحديد الاستعلام المراد تنفيذه SELECT FirstName, LastName, Department FROM Northwind.vEmployeeDepartment WHERE FirstName = @FirstName AND LastName = @LastName AND EndDate IS NULL; يمكن استدعاء الإجراء على النحو التالي: EXECUTE Northwind.getEmployee N'Ackerman', N'Pilar'; -- أو EXEC Northwind.getEmployee @LastName = N'Ackerman', @FirstName = N'Pilar'; GO -- أو EXECUTE Northwind.getEmployee @FirstName = N'Pilar', @LastName = N'Ackerman'; GO الزنادات Triggers الزنادات هي إجراءات مخزّنة تُستدعى تلقائيًا عند وقوع أحداث معينة، مثل، إدراج صفّ في عمود، أو تحديث عمود ما، أو غيرها من الأحداث. إنشاء زناد ينشئ هذا المثال زنادًا يُدرج سجلًا في جدول ثانٍ (MyAudit) عند إدراج سجل ما في الجدول الذي عُرِّف الزناد عليه (MyTable). في هذا المثال، الجدول "inserted" هو جدول خاص تستخدمه Microsoft SQL Server لتخزين الصفوف المتأثِّرة (affected rows) خلال عبارتي INSERT و UPDATE؛ يوجد أيضًا جدول "deleted" خاصّ يؤدي نفس الوظيفة في عبارات DELETE. CREATE TRIGGER MyTrigger ON MyTable AFTER INSERT AS BEGIN -- MyAudit إضافة سجل إلى الجدول INSERT INTO MyAudit(MyTableId, User) (SELECT MyTableId, CURRENT_USER FROM inserted) END المثال التالي يستخدم زنادًا لإدارة سلة المحذوفات عبر الجدول "deleted": CREATE TRIGGER BooksDeleteTrigger ON MyBooksDB.Books AFTER DELETE AS INSERT INTO BooksRecycleBin SELECT * FROM deleted; GO المعامَلات Transactions المعامَلات (Transactions) هي سلسلة من عمليات SQL تُجرى على قاعدة بيانات، هذه السلسلة تُعامل كما لو كانت عملية واحدة، بحيث إما أن تُنفَّذ جميعا، ونقول أنّه تمّ الالتزام بها (committed)، أو عدم تنفيذ أيّ منها، ونقول أنّه تمّ التراجع عنها (rolled back). المثال التالي يوضّح معاملة بسيطة: BEGIN TRANSACTION INSERT INTO DeletedEmployees(EmployeeID, DateDeleted, User) (SELECT 123, GetDate(), CURRENT_USER); DELETE FROM Employees WHERE EmployeeID = 123; COMMIT TRANSACTION يمكنك التراجع عن المعاملة في حال حدث خطأ في الشيفرة: BEGIN TRY BEGIN TRANSACTION INSERT INTO Users(ID, Name, Age) VALUES(1, 'Bob', 24) DELETE FROM Users WHERE Name = 'Todd' COMMIT TRANSACTION END TRY BEGIN CATCH ROLLBACK TRANSACTION END CATCH ترجمة -وبتصرّف- للفصول من 52 إلى 56 من الكتاب SQL Notes for Professionals اقرأ المقال: المقال التالي: تصميم الجداول ومعلومات المخطط وترتيب تنفيذ الاستعلامات في SQL المقال السابق: مواضيع متفرقة في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  15. تستعرض هذه المقالة مجموعة من المواضيع الإضافية في SQL، مثل العروض (Views)، وكيفية كتابة التعليقات، وكيفية التعامل مع المفاتيح الخارجية (Foreign Keys) وإنشاء السلاسل. العروض Views العروض البسيطة تُستخدم العروض (View) لترشيح الصفوف من الجدول الأساسي، أو الاكتفاء بعرض بعض الأعمدة منه فقط: CREATE VIEW new_employees_details AS SELECT E.id, Fname, Salary, Hire_date FROM Employees E WHERE hire_date > date '2015-01-01'; يختار (select) المثالُ التالي من النتائج المعروضة في العرض (view): select * from new_employees_details الخرج الناتج: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } Id FName Salary Hire_date 4 Johnathon 500 24-07-2016 العروض المركبة Complex views يمكن أن تكون العروض معقدة ومركّبة (تجميعات aggregations، عمليات ضمّ، استعلامات فرعية، إلخ). المهم أن تتأكّد دائمًا من إضافة أسماء الأعمدة لكل شيء تختاره: Create VIEW dept_income AS SELECT d.Name as DepartmentName, sum(e.salary) as TotalSalary FROM Employees e JOIN Departments d on e.DepartmentId = d.id GROUP BY d.Name; يمكنك الآن الاختيار (SELECT) كما تختار من أيّ جدول عادي: SELECT * FROM dept_income; الخرج الناتج: DepartmentName TotalSalary HR 1900 Sales 600 العروض المادية Materialized Views العروض المادية هي العروض التي تكون نتائجها مُخزّنة في ذاكرة مادية، وتُحدّث دوريًا حتى تظل متزامنة مع القيم الحالية. العروض المادّية مفيدة في تخزين نتائج الاستعلامات المعقّدة طويلة الأمد التي لا تعتمد على نتائج الوقت الحقيقي (realtime results). يمكن إنشاء العروض المادية في Oracle و PostgreSQL. كما توفّر أنظمة قواعد البيانات الأخرى وظائف مماثلة، مثل العروض المُفهرسة (indexed views) في SQL Server، أو جداول الاستعلام المادية (materialized query tables) في DB2. هذا مثال على العروض المادية في PostgreSQL: CREATE TABLE mytable (number INT); INSERT INTO mytable VALUES (1); CREATE MATERIALIZED VIEW myview AS SELECT * FROM mytable; SELECT * FROM myview; number -------- 1 (1 row) INSERT INTO mytable VALUES(2); SELECT * FROM myview; number -------- 1 (1 row) REFRESH MATERIALIZED VIEW myview; SELECT * FROM myview; number -------- 1 2 (2 rows) التعليقات هناك نوعان من التعليقات في SQL، التعليقات السطرية، والتعليقات متعددة الأسطر. التعليقات السطرية Single-line comments التعليقات السطرية هي تعليقات تستمر حتى نهاية السطر، وتُسبَق بالرمز ‎--‎: SELECT * FROM Employees -- هذا تعليق WHERE FName = 'John' التعليقات متعددة الأسطر Multi-line comments تُوضع التعليقات متعددة الأسطر داخل الغلاف ‎/* ... */‎: /* يعيد هذا الاستعلام جميع الموظفين */ SELECT * FROM Employees يجوز أيضًا إدراج مثل هذا التعليق في منتصف السطر: SELECT /* جميع الأعمدة: */ * FROM Employees المفاتيح الخارجية Foreign Keys تضمن قيود المفاتيح الخارجية (Foreign Keys constraints) تكامل البيانات، إذ تفرض أن تتطابق القيم الموجودة في جدول معيّن، مع القيم المقابلة في جدول آخر. مثلا، في الجامعة، تنتمي كل دورة دراسية إلى قسم معيّن. يمكننا التعبير عن هذا القيد (constraint) على النحو التالي: CREATE TABLE Department ( Dept_Code CHAR (5) PRIMARY KEY, Dept_Name VARCHAR (20) UNIQUE ); المثال التالي يدرج قيمًا جديدة في قسم علوم الحاسوب: INSERT INTO Department VALUES ('CS205', 'Computer Science'); يحتوي الجدول التالي على معلومات عن المواضيع التي تشملها شعبة علوم الحاسوب: CREATE TABLE Programming_Courses ( Dept_Code CHAR(5), Prg_Code CHAR(9) PRIMARY KEY, Prg_Name VARCHAR (50) UNIQUE, FOREIGN KEY (Dept_Code) References Department(Dept_Code) ); (يجب أن يتطابق نوع بيانات المفتاح الخارجي مع نوع البيانات الخاص بالمفتاح المشار إليه - referenced key.) لا يسمح قيد المفتاح الخارجي الخاص بالعمود ‎Dept_Code‎ إلّا بالقيم الموجودة سلفًا في الجدول المشار إليه. هذا يعني أنه إذا حاولت إدراج القيم التالية: INSERT INTO Programming_Courses Values ('CS300', 'FDB-DB001', 'Database Systems'); فستطرح قاعدة البيانات خطأ انتهاك المفتاح الخارجي (Foreign Key violation error)، لأنّ ‎CS300‎ غير موجودة في جدول الأقسام ‎Department‎. ولكن عند تجربة قيمة مفتاح موجود: INSERT INTO Programming_Courses VALUES ('CS205', 'FDB-DB001', 'Database Systems'); INSERT INTO Programming_Courses VALUES ('CS205', 'DB2-DB002', 'Database Systems II'); فلن يكون هناك أيّ مشكلة. هذه بعض النصائح حول كيفية استخدام المفاتيح الخارجية: يجب أن يشير المفتاح الخارجي إلى مفتاح فريد - UNIQUE - (أو أساسي - PRIMARY) من الجدول الأصلي الأب (parent table). لن ينجم أيّ خطأ عن إدخال القيمة المعدومة NULL إلى عمود المفتاح الخارجي. يمكن أن تشير قيود المفاتيح الخارجية إلى الجداول الموجودة في نفس قاعدة البيانات. يمكن أن تشير قيود المفتاح الخارجي إلى عمود آخر في نفس الجدول (مرجع ذاتي). إنشاء جدول بمفتاح خارجي في هذا المثال، لدينا جدول البيانات ‎SuperHeros‎. يحتوي هذا الجدول على مفتاح أساسي ‎ID‎. سنضيف جدولًا جديدًا بُغية تخزين صلاحيات كل بطل خارق: CREATE TABLE HeroPowers ( ID int NOT NULL PRIMARY KEY, Name nvarchar(MAX) NOT NULL, HeroId int REFERENCES SuperHeros(ID) ) في هذا المثال، يُعدّ العمود ‎HeroId‎ مفتاحًا خارجيًا للجدول ‎SuperHeros‎. التسلسلات Sequences التسلسلات هي سلاسل من الأعداد. المثال التالي ينشئ تسلسلًا يبدأ من 1000، ويتزايد بمقدار 1. CREATE SEQUENCE orders_seq START WITH 1000 INCREMENT BY 1; في المثال التالي، سنستخدم مرجعًا (seq_name.NEXTVAL) يشير إلى القيمة التالية في التسلسل. تنبيه: في كلّ عبارة، تكون هناك قيمة واحدة فقط من التسلسل. أي أنّه إذا كانت هناك عدة مراجع إلى القيمة التالية في التسلسل (NEXTVAL) في عبارة معينة، فستشير جميع تلك المراجع إلى نفس الرقم من السلسلة. يمكن استخدام القيمة التالية NEXTVAL في عبارات الإدراج INSERTS: INSERT INTO Orders (Order_UID, Customer) VALUES (orders_seq.NEXTVAL, 1032); كما يمكن أن تُستخدم في عمليات التحديث: UPDATE Orders SET Order_UID = orders_seq.NEXTVAL WHERE Customer = 581; ويمكن أيضًا أن تُستخدم في عبارات الاختيار SELECT: SELECT Order_seq.NEXTVAL FROM dual; ترجمة -وبتصرّف- للفصول من 47 إلى 51 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: الاستعلامات الفرعية والإجراءات في SQL المقال السابق: التعابير الجدولية الشائعة Common Table Expressions في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  16. توليد قيم لا توفّر معظم قواعد البيانات طريقة أصلية لإنشاء سلاسل الأرقام؛ بيْد أنّه يمكن استخدام تعبيرات الجدول الشائعة أو التعبيرات الجدولية (common table expressions) مع العوديّة (recursion) لمحاكاة هذا النوع من الوظائف. يولّد المثال التالي تعبيرًا جدوليًا يُسمّى ‎Numbers‎، واسم عموده ‎i‎، ويحتوي أرقام الصفوف (1-5): -- لتخزين الأعداد `i` واسم العمود `Numbers"إعطاء اسم الجدول WITH Numbers(i) AS ( -- البداية SELECT 1 -- ضروري لأجل العودية UNION ALL المعامل UNION ALL -- تعبير التكرار SELECT i + 1 -- التعبير الجدولي الذي أعلنا عنه والمُستخدم كمصدر للعودية FROM Numbers -- عبارة إنهاء العودية WHERE i < 5 ) -- استخدام التعبير الجدولي المُنشأ كما لو كان جدولا عاديا SELECT i FROM Numbers; الخرج الناتج: i 1 2 3 4 5 يمكن استخدام هذه الطريقة مع أي مجال من الأعداد، وكذلك مع أنواع أخرى من البيانات. الترقيم العودي لشجيرة recursively enumerating a subtree المثال التالي يوضّح كيفية ترقيم شجيرة (subtree) عوديًا: WITH RECURSIVE ManagedByJames(Level, ID, FName, LName) AS ( -- البدء بهذا الصف SELECT 1, ID, FName, LName FROM Employees WHERE ID = 1 UNION ALL -- الحصول على الموظفين الذين يعملون تحت إمرة أيّ من المدراء المُختارين سابقا SELECT ManagedByJames.Level + 1, Employees.ID, Employees.FName, Employees.LName FROM Employees JOIN ManagedByJames ON Employees.ManagerID = ManagedByJames.ID ORDER BY 1 DESC -- depth-first search البحث الأولي-العميق ) SELECT * FROM ManagedByJames; الخرج الناتج: Level ID FName LName 1 1 James Smith 2 2 John Johnson 3 4 Johnathon Smith 2 3 Michael Williams الاستعلامات المؤقتة Temporary query تتصرف الاستعلامات المؤقتة (Temporary query) مثل الاستعلامات المتشعّبة (nested subqueries)، إلّا أنّ صياغتها مختلفة. WITH ReadyCars AS ( SELECT * FROM Cars WHERE Status = 'READY' ) SELECT ID, Model, TotalCost FROM ReadyCars ORDER BY TotalCost; الخرج الناتج: ID Model TotalCost 1 Ford F-150 200 2 Ford F-150 230 هذا استعلام فرعي مكافئ: SELECT ID, Model, TotalCost FROM ( SELECT * FROM Cars WHERE Status = 'READY' ) AS ReadyCars ORDER BY TotalCost التسلّق العوديّ لشجرة recursively going up in a tree المثال التالي يوضّح كيفية تسلق شجرة عوديًا: WITH RECURSIVE ManagersOfJonathon AS ( -- البدء بهذا الصف SELECT * FROM Employees WHERE ID = 4 UNION ALL -- الحصول على مدراء كل الصفوف المُختارة سابقا SELECT Employees.* FROM Employees JOIN ManagersOfJonathon ON Employees.ID = ManagersOfJonathon.ManagerID ) SELECT * FROM ManagersOfJonathon; الخرج الناتج: Id FName LName PhoneNumber ManagerId DepartmentId 4 Johnathon Smith 1212121212 2 1 2 John Johnson 2468101214 1 1 1 James Smith 1234567890 NULL 1 table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } التوليد العودي للتواريخ المثال التالي يولّد تواريخ مع تضمين الجداول الزمنية لفِرَق العمل: DECLARE @DateFrom DATETIME = '2016-06-01 06:00' DECLARE @DateTo DATETIME = '2016-07-01 06:00' DECLARE @IntervalDays INT = 7 -- Transition Sequence = وقت الاستراحة في المناوبات الليلية والنهارية -- RR (Rest & Relax) = 1 -- DS (Day Shift) = 2 -- NS (Night Shift) = 3 ;WITH roster AS ( SELECT @DateFrom AS RosterStart, 1 AS TeamA, 2 AS TeamB, 3 AS TeamC UNION ALL SELECT DATEADD(d, @IntervalDays, RosterStart), CASE TeamA WHEN 1 THEN 2 WHEN 2 THEN 3 WHEN 3 THEN 1 END AS TeamA, CASE TeamB WHEN 1 THEN 2 WHEN 2 THEN 3 WHEN 3 THEN 1 END AS TeamB, CASE TeamC WHEN 1 THEN 2 WHEN 2 THEN 3 WHEN 3 THEN 1 END AS TeamC FROM roster WHERE RosterStart < DATEADD(d, -@IntervalDays, @DateTo) ) SELECT RosterStart, ISNULL(LEAD(RosterStart) OVER (ORDER BY RosterStart), RosterStart + @IntervalDays) AS RosterEnd, CASE TeamA WHEN 1 THEN 'RR' WHEN 2 THEN 'DS' WHEN 3 THEN 'NS' END AS TeamA, CASE TeamB WHEN 1 THEN 'RR' WHEN 2 THEN 'DS' WHEN 3 THEN 'NS' END AS TeamB, CASE TeamC WHEN 1 THEN 'RR' WHEN 2 THEN 'DS' WHEN 3 THEN 'NS' END AS TeamC FROM roster النتيجة المُعادة: استخدام CONNECT BY في Oracle مع تعبير جدولي عودي توفّر الوظيفة CONNECT BY المُستخدمة في Oracle العديد من الميزات المفيدة التي لا يوجد لها مثيل في التعبيرات الجدولية العودية القياسية في SQL. يحاول هذا المثال محاكاة هذه الميزات (مع بعض الإضافات التكميلية) باستخدام صياغة SQL Server. هذه الوظائف مفيدة للغاية لمطوّري Oracle - إذ توفّر لهم العديد من الميزات في الاستعلامات المتشعبة (hierarchical queries) غير الموجودة في قواعد البيانات الأخرى، كما أنّها مفيدة أيضًا في توضيح استخدامات الاستعلامات المتشعبة عمومًا. WITH tbl AS ( SELECT id, name, parent_id FROM mytable) , tbl_hierarchy AS ( /* Anchor */ SELECT 1 AS "LEVEL" --, 1 AS CONNECT_BY_ISROOT --, 0 AS CONNECT_BY_ISBRANCH , CASE WHEN t.id IN (SELECT parent_id FROM tbl) THEN 0 ELSE 1 END AS CONNECT_BY_ISLEAF , 0 AS CONNECT_BY_ISCYCLE , '/' + CAST(t.id AS VARCHAR(MAX)) + '/' AS SYS_CONNECT_BY_PATH_id , '/' + CAST(t.name AS VARCHAR(MAX)) + '/' AS SYS_CONNECT_BY_PATH_name , t.id AS root_id , t.* FROM tbl t WHERE t.parent_id IS NULL -- START WITH parent_id IS NULL UNION ALL /* العودية*/ SELECT th."LEVEL" + 1 AS "LEVEL" --, 0 AS CONNECT_BY_ISROOT --, CASE WHEN t.id IN (SELECT parent_id FROM tbl) THEN 1 ELSE 0 END AS CONNECT_BY_ISBRANCH , CASE WHEN t.id IN (SELECT parent_id FROM tbl) THEN 0 ELSE 1 END AS CONNECT_BY_ISLEAF , CASE WHEN th.SYS_CONNECT_BY_PATH_id LIKE '%/' + CAST(t.id AS VARCHAR(MAX)) + '/%' THEN 1 ELSE 0 END AS CONNECT_BY_ISCYCLE , th.SYS_CONNECT_BY_PATH_id + CAST(t.id AS VARCHAR(MAX)) + '/' AS SYS_CONNECT_BY_PATH_id , th.SYS_CONNECT_BY_PATH_name + CAST(t.name AS VARCHAR(MAX)) + '/' AS SYS_CONNECT_BY_PATH_name , th.root_id , t.* FROM tbl t JOIN tbl_hierarchy th ON (th.id = t.parent_id) -- CONNECT BY PRIOR id = parent_id WHERE th.CONNECT_BY_ISCYCLE = 0) -- NOCYCLE SELECT th.* --, REPLICATE(' ', (th."LEVEL" - 1) * 3) + th.name AS tbl_hierarchy FROM tbl_hierarchy th JOIN tbl CONNECT_BY_ROOT ON (CONNECT_BY_ROOT.id = th.root_id) ORDER BY th.SYS_CONNECT_BY_PATH_name; -- ORDER SIBLINGS BY name هذا شرح لميزات CONNECT BY الموضّحة أعلاه: العبارات CONNECT BY: تحدّد العلاقة التي تعرّف التشعّب START WITH: تحدّد العقدة الجذرية (root nodes). ORDER SIBLINGS BY: تحدّد ترتيب النتائج المعاملات NOCYCLE: توقِف معالجة فرع معيّن عند رصد شعبة دورية (loop). لأنّ الشعب الصالحة هي الشعب غير الدورية (Directed Acyclic)، أي الشعب التي لا يمكن العودة عبرها إلى العقدة نفسها. العمليات PRIOR: تحصل على البيانات من العقدة الأب (node's parent). CONNECT_BY_ROOT: تحصل على البيانات من العقدة الجذرية. أشباه الأعمدة Pseudocolumns LEVEL: تشير إلى مسافة العقدة من جذرها. CONNECT_BY_ISLEAF: تشير إلى عقدة بدون فروعها. CONNECT_BY_ISCYCLE: تشير إلى عقدة ذات مرجع دائري (circular reference). الدوال SYS_CONNECT_BY_PATH: تعيد سلسلة نصية تمثّل المسار من الجذر إلى العقدة. ترجمة -وبتصرّف- للفصل 46 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: مواضيع متفرقة في SQL المقال السابق: دوال التعامل مع النصوص في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  17. الدوال النصية String Functions هي دوال تُنفَّذ على قيم نصية، وتعيد إمّا قيمًا عددية أو قيمًا نصية. مثلًا، يمكن استخدام الدوال النصية لدمج البيانات، أو استخراج أجزاء من السلاسل النصية، أو موازنة السلاسل النصية أو تحويلها من الأحرف الكبيرة إلى الصغيرة، أو العكس. ضم السلاسل النصية في SQL القياسية ‏‎(ANSI / ISO)‎‎، يُرمز لمعامل ضمّ السلاسل النصية (string concatenation) بالرمز ‎||‎. وهذه الصياغة مدعومة من قبل كافّة أنظمة معالجة قواعد البيانات الرئيسية خلا SQL Server: SELECT 'Hello' || 'World' || '!'; -- ==> HelloWorld! تدعم العديد من أنظمة معالجة قواعد البيانات الدالة ‎CONCAT‎ التي تضمّ السلاسل النصية: SELECT CONCAT('Hello', 'World'); -- ==> 'HelloWorld' تدعم أيضًا بعض قواعد البيانات استخدام ‎CONCAT‎ لضمّ أكثر من سلسلتين نصيتين (باستثناء Oracle): SELECT CONCAT('Hello', 'World', '!'); -- ==> 'HelloWorld!' في بعض أنظمة معالجة قواعد البيانات، يجب تحويل الأنواع غير النصبة قبل ضمّها: SELECT CONCAT('Foo', CAST(42 AS VARCHAR(5)), 'Bar'); -- ==> 'Foo42Bar' تجري بعض قواعد البيانات (مثل Oracle) تحويلات ضمنيّة غير مُفرِِّطة (implicit lossless conversions)، أي لا ينتج عنها أيّ ضياع للبيانات. على سبيل المثال، يعيد تطبيق الدالة ‎CONCAT‎ على النوعين ‎CLOB‎ و ‎NCLOB‎ قيمة من النوع ‎NCLOB‎. فيما يعيد تطبيق الدالة ‎CONCAT‎ على عدد، وعلى قيمةٍ من النوع ‎varchar2‎ قيمةً من النوع ‎varchar2‎: SELECT CONCAT(CONCAT('Foo', 42), 'Bar') FROM dual; -- ==> Foo42Bar يمكن لبعض قواعد البيانات استخدام المعامل ‎+‎ غير القياسي (في معظم الأحيان مع الأعداد وحسب): SELECT 'Foo' + CAST(42 AS VARCHAR(5)) + 'Bar'; لا تدعم إصدارات SQL Server قبل 2012 الدالة ‎CONCAT‎، لذا فإنّ المعامل ‎+‎ هو الطريقة الوحيدة لضمّ السلاسل النصية فيها: طول سلسلة نصية SQL Server تُستخدم الدالة LEN لحساب طول سلسلة نصية، بيد أنّها لا تحسُب المسافات البيضاء الزائدة. SELECT LEN('Hello') -- 5 SELECT LEN('Hello '); -- 5 على خلاف LEN، تحسب الدالة DATALENGTH طول سلسلة نصية بما فيها المسافات الزائدة: SELECT DATALENGTH('Hello') -- 5 SELECT DATALENGTH('Hello '); -- 6 تجدر الإشارة إلى أنّ الدالة DATALENGTH تُعيد طول التمثيل البتّي (byte representation) في الذاكرة للسلسلة النصية، والذي تتعلق قيمته بمجموعة المحارف (charset) المستخدمة لتخزين السلسلة النصية. DECLARE @str varchar(100) = 'Hello ' SELECT DATALENGTH(@str) -- 6 DECLARE @nstr nvarchar(100) = 'Hello ' SELECT DATALENGTH(@nstr) -- 12 عادة ما تكون قيم varchar عبارة عن سلسلة نصية من النوع ASCII حيث يخزن كل حرف في بايت، وعادة ما تكون قيم nvarchar عبارة عن سلسلة نصية من النوع unicode حيث يخزن كل حرف في بايتين. Oracle يستخدم نظام Oracle الدالة Length لحساب طول سلسلة نصية: SELECT Length('Bible') FROM dual; -- 5 SELECT Length('righteousness') FROM dual; -- 13 SELECT Length(NULL) FROM dual; -- NULL تقليم المسافات الفارغة تُستخدم الدالة Trim لإزالة المسافات البيضاء الموجودة في بداية أو نهاية نتائج الاستعلام. توجد في MSSQL عدّة دوال للتقليم كما يوضّح المثال التالي: SELECT LTRIM(' Hello ') -- ==> 'Hello ' SELECT RTRIM(' Hello ') -- ==> ' Hello' SELECT LTRIM(RTRIM(' Hello ')) -- ==> 'Hello' هذا المثال يعمل في MySql و Oracle: SELECT TRIM(' Hello ') -- ==> 'Hello' الدالتان UPPER و LOWER تحوّل الدالة UPPER سلسلة نصية إلى سلسلة نصية ذات أحرف كبيرة، أما LOWER فتفعل العكس: SELECT UPPER('HelloWorld') -- ==> 'HELLOWORLD' SELECT LOWER('HelloWorld') -- ==> 'helloworld' تقسيم السلاسل النصية تقسّم الدالة SPLIT السلسلة النصية بحسب فاصل حرفي. لاحظ أنّ ‎STRING_SPLIT()‎ دالةٌ جدولية (table-valued function). SELECT value FROM STRING_SPLIT('Lorem ipsum dolor sit amet.', ' '); سنحصل على النتيجة التالية: value ----- Lorem ipsum dolor sit amet. الاستبدال تستبدل الدالة ‎REPLACE‎ سلسلة نصية بأخرى. وتُصاغ وفق الشكل التالي: REPLACE( S , O , R ) S: السلسلة النصية التي سيُبحَث فيها O: السلسلة النصية المراد استبدالها R: السلسلة النصية المراد وضعها مكان السلسلة الأصلية: SELECT REPLACE( 'Peter Steve Tom', 'Steve', 'Billy' ) -- Peter Billy Tom التعابير النمطية REGEXP MySQL ≥ 3.19 تُستخدم REGEXP للتحقّق ممّا إذا كانت السلسلة النصية تتطابق مع تعبير نمطي - regular expression - (داخل سلسلة نصّية أخرى). SELECT 'bedded' REGEXP '[a-f]' -- True SELECT 'beam' REGEXP '[a-f]' -- False السلاسل النصية الفرعية Substrings تعيد الدالة ‎SUBSTRING جزءًا من سلسلة نصية كما يوَضّح المثال التالي: SELECT SUBSTRING('Hello', 1, 2) -- ==> 'He' SELECT SUBSTRING('Hello', 3, 3) -- ==> 'llo' تنبيه: تبدأ فهارس السلاسل النصية في SQL من القيمة 1. غالبًا ما تُستخدم SUBSTRING مع الدّالة ‎LEN()‎ للحصول على آخر ‎n‎ حرف من سلسلة نصية ذات طول غير معروف. DECLARE @str1 VARCHAR(10) = 'Hello', @str2 VARCHAR(10) = 'FooBarBaz'; SELECT SUBSTRING(@str1, LEN(@str1) - 2, 3) -- ==> 'llo' SELECT SUBSTRING(@str2, LEN(@str2) - 2, 3) -- ==> 'Baz' Stuff تحشر الدالة Stuff سلسلة نصّية داخل أخرى، إذ تستبدل 0 حرف أو أكثر في موضع معين. تنبيه: يُحسب الموضع ‎start‎ انطلاقا من القيمة 1. هذه صياغة الدالة: STUFF ( character_expression , start , length , replaceWith_expression ) يحشر المثال التوضيحي التالي السلسلة النصية 'Hello' في الموضع 4 من السلسلة 'FooBarBaz' ويضعها مكان Bar: SELECT STUFF('FooBarBaz', 4, 3, 'Hello') -- ==> 'FooHelloBaz' الدالتان LEFT و RIGHT تعيد الدالة RIGHT آخر n حرف من سلسلة نصية، فيما تعيد LEFT أول n حرف من سلسلة نصية: إليك المثال التالي: SELECT LEFT('Hello',2) -- He SELECT RIGHT('Hello',2) -- lo لا يحتوي نظام Oracle SQL على الدالتين LEFT و RIGHT. بيْد أنّه يمكن محاكاتهما باستخدام الدالتين SUBSTR و LENGTH على النحو التالي: SELECT SUBSTR('Hello',1,2) -- He SELECT SUBSTR('Hello',LENGTH('Hello')-2+1,2) -- lo عكس سلسلة نصية تعكس الدالة REVERSE السلاسل النصية: SELECT REVERSE('Hello') -- ==> olleH تكرار سلسلة نصية تضمّ الدالة ‎REPLICATE‎ سلسلة نصية إلى نفسها عددًا محدّدًا من المرّات كما يوضّح المثال التالي: SELECT REPLICATE ('Hello',4) -- ==> 'HelloHelloHelloHello' استخدام الدالة Replace مع Select و Update تُستخدم الدالة REPLACE في SQL لتحديث محتوى سلسلة نصية. وتُستدعى هذه الدالة عبر الصياغة REPLACE()‎ في MySQL و Oracle و SQL Server. وتُصاغ على النحو التالي: REPLACE (str, find, repl) يستبدل المثال التالي تكرارات السلسلة النصية ‎South‎ بـ ‎Southern‎ في جدول الموظفين Employees: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } FirstName Address James South New York John South Boston Michael South San Diego استخدام REPLACE مع Select إليك الاستعلام التالي: SELECT FirstName, REPLACE (Address, 'South', 'Southern') Address FROM Employees ORDER BY FirstName سنحصل على النتيجة التالية: FirstName Address James Southern New York John Southern Boston Michael Southern San Diego استخدام REPLACE مع Update يمكننا استخدام الدالة REPLACE لإجراء تغييرات دائمة في الجدول على النحو التالي: Update Employees Set city = (Address, 'South', 'Southern'); هناك مقاربة أخرى أشهر تتمثل في استخدام REPLACE مع عبارة WHERE على النحو التالي: Update Employees Set Address = (Address, 'South', 'Southern') Where Address LIKE 'South%'; INSTR تعيد هذه الدالة فهرس أول ظهور لسلسلة نصية فرعية (أو تعيد 0 إن لم يُعثر عليها). SELECT INSTR('FooBarBar', 'Bar') -- 4 SELECT INSTR('FooBarBar', 'Xar') -- 0 PARSENAME SQL Server تعيد الدالة PARSENAME جزءًا محدّدًا من كائن نصي - string(object name)‎‎ -. قد يحتوي اسم الكائن - object name - على اسم كائن شبه نصّي (string like object)، أو اسم المالك (owner name) أو اسم قاعدة البيانات أو اسم الخادم. يمكنك معرفة المزيد من التفاصيل من الرابط: MSDN:PARSENAME هذه صياغة الدالة PARSENAME: PARSENAME('NameOfStringToParse',PartIndex) يمكنك العثور على اسم الكائن، في الفهرس رقم ‎1‎: SELECT PARSENAME('ServerName.DatabaseName.SchemaName.ObjectName',1) -- `ObjectName` SELECT PARSENAME('[1012-1111].SchoolDatabase.school.Student',1) -- `Student` للحصول على اسم المخطط (schema)، استخدم الفهرس ‎2‎: SELECT PARSENAME('ServerName.DatabaseName.SchemaName.ObjectName',2) -- `SchemaName` SELECT PARSENAME('[1012-1111].SchoolDatabase.school.Student',2) -- `school` للحصول على اسم قاعدة البيانات، استخدم الفهرس ‎3‎: SELECT PARSENAME('ServerName.DatabaseName.SchemaName.ObjectName',3) -- `DatabaseName` SELECT PARSENAME('[1012-1111].SchoolDatabase.school.Student',3) -- `SchoolDatabase` للحصول على اسم الخادم، استخدم الفهرس ‎4‎: SELECT PARSENAME('ServerName.DatabaseName.SchemaName.ObjectName',4) -- `ServerName` SELECT PARSENAME('[1012-1111].SchoolDatabase.school.Student',4) -- `[1012-1111]` تعيد PARSENAME قيمة معدومة (null) في حال كان الجزء المُعيّن غير موجود في الكائن النصي. ترجمة -وبتصرّف- للفصل 41 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: التعابير الجدولية الشائعة Common Table Expressions في SQL المقال السابق: دوال التعامل مع البيانات في SQL النسخة الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  18. تستعرض هذه المقالة عددًا من أنواع الدوال، مثل الدوال التجميعية (Aggregate Functions) والدوال التحليلية (Analytic Functions) والدوال العددية. الدوال التجميعية aggregate functions تستعرض هذه الفقرة مجموعة من الدوال التجميعية المُستخدمة في SQL، وهي دوال تأخذ مجموعة من القيم، وتعيد قيمة واحدة. التجميع الشرطي Conditional aggregation إليك جدول المدفوعات التالي: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } Customer Payment_type Amount Peter Credit 100 Peter Credit 300 John Credit 1000 John Debit 500 تحسب الشيفرة التالية المجموع الكلي لرصيد أو دين كل موظف في الجدول: select customer, sum(case when payment_type = 'credit' then amount else 0 end) as credit, sum(case when payment_type = 'debit' then amount else 0 end) as debit from payments group by customer سنحصل على النتيجة التالية: Customer Credit Debit Peter 400 0 John 1000 500 إليك الآن المثال التالي: select customer, sum(case when payment_type = 'credit' then 1 else 0 end) as credit_transaction_count, sum(case when payment_type = 'debit' then 1 else 0 end) as debit_transaction_count from payments group by customer هذا هو الخرج الناتج: Customer credit_transaction_count debit_transaction_count Peter 2 0 John 1 1 دورة علوم الحاسوب دورة تدريبية متكاملة تضعك على بوابة الاحتراف في تعلم أساسيات البرمجة وعلوم الحاسوب اشترك الآن ضمّ القوائم List Concatenation تجمّع عملية ضمّ القوائم (List Concatenation) عناصر عمود أو تعبيرًا عن طريق دمج القيم في سلسلة نصية واحدة لكل مجموعة. يمكن أيضًا تحديد سلسلة نصية لفصل القيم (إما سلسلة نصية فارغة أو فاصلة عند حذفها)، كما يمكن تحديد ترتيب القيم المُعادة. ورغم أنّها ليست جزءًا من معيار SQL القياسي، إلا أنّ كلّ أنظمة قواعد البيانات العلائقية تدعمها. MySQL SELECT ColumnA , GROUP_CONCAT(ColumnB ORDER BY ColumnB SEPARATOR ',') AS ColumnBs FROM TableName GROUP BY ColumnA ORDER BY ColumnA; Oracle و DB2 SELECT ColumnA , LISTAGG(ColumnB, ',') WITHIN GROUP (ORDER BY ColumnB) AS ColumnBs FROM TableName GROUP BY ColumnA ORDER BY ColumnA; PostgreSQL SELECT ColumnA , STRING_AGG(ColumnB, ',' ORDER BY ColumnB) AS ColumnBs FROM TableName GROUP BY ColumnA ORDER BY ColumnA; SQL Server قبل 2016 WITH CTE_TableName AS ( SELECT ColumnA, ColumnB FROM TableName) SELECT t0.ColumnA , STUFF(( SELECT ',' + t1.ColumnB FROM CTE_TableName t1 WHERE t1.ColumnA = t0.ColumnA ORDER BY t1.ColumnB FOR XML PATH('')), 1, 1, '') AS ColumnBs FROM CTE_TableName t0 GROUP BY t0.ColumnA ORDER BY ColumnA; SQL Server 2017 و SQL Azure SELECT ColumnA , STRING_AGG(ColumnB, ',') WITHIN GROUP (ORDER BY ColumnB) AS ColumnBs FROM TableName GROUP BY ColumnA ORDER BY ColumnA; SQLite بدون ترتيب: SELECT ColumnA , GROUP_CONCAT(ColumnB, ',') AS ColumnBs FROM TableName GROUP BY ColumnA ORDER BY ColumnA; يتطلب الترتيب استخدام استعلامً فرعي (subquery)، أو تعبيرًا جدوليًا CTE، وهو مجموعة ننائج مؤقتة يمكنك الرجوع إليها داخل عبارات SELECT أو INSERT أو UPDATE أو DELETE الأخرى: WITH CTE_TableName AS ( SELECT ColumnA, ColumnB FROM TableName ORDER BY ColumnA, ColumnB) SELECT ColumnA , GROUP_CONCAT(ColumnB, ',') AS ColumnBs FROM CTE_TableName GROUP BY ColumnA ORDER BY ColumnA; SUM تجمع الدالة ‎Sum‎ قيم صفوف مجموعة النتائج. وفي حال حذف العبارة group by، فستُجمَع قيم كلّ الصفوف. المثال التالي لا يستخدم العبارة group by: select sum(salary) TotalSalary from employees; سنحصل على الخرج التالي: TotalSalary 2500 إليك مثال يستخدم group by: select DepartmentId, sum(salary) TotalSalary from employees group by DepartmentId; الخرج الناتج: DepartmentId TotalSalary 1 2000 2 500 المتوسط AVG تعيد الدالة التجميعية ‎‎AVG()‎‎ متوسط قيم تعبير معيّن، والتي عادةً ما تكون قيمًا رقمية في عمود. لنفترض أنّ لدينا جدولًا يحتوي على تعداد سكان مدن العالم. مثلا، سجلّ مدينة نيويورك سيكون من هذا القبيل: city_name population year New York City 8,550,405 2015 New York City ... ... New York City 8,000,906 2005 يحسب الاستعلام التالي متوسط عدد سكان مدينة نيويورك في الولايات المتحدة الأمريكية في السنوات العشر الماضية: select city_name, AVG(population) avg_population from city_population where city_name = 'NEW YORK CITY'; لاحظ كيف لم توضع السنة في الاستعلام، وذلك لأنّنا نريد حساب متوسط عدد السكان بمرور الوقت. سنحصل على النتائج التالية: city_name avg_population New York City 8,250,754 تنبيه: تحوّل الدالة AVG القيم إلى أعداد، وهذا أمر ينبغي أن تأخذه بالحسبان دائمًا، خصوصا عندما تعمل بقيم التاريخ والوقت. Count يمكنك استخدام الدالة Count لحساب عدد الصفوف: SELECT count(*) TotalRows FROM employees; النتيجة: TotalRows 4 يعدّ المثال التالي الموظفين في كل قسم: SELECT DepartmentId, count(*) NumEmployees FROM employees GROUP BY DepartmentId; الخرج الناتج: DepartmentId NumEmployees 1 3 2 1 يمكنك العدّ بحسب الأعمدة أو التعابير مع عدم احتساب القيم المعدومة ‎NULL‎: SELECT count(ManagerId) mgr FROM EMPLOYEES; النتيجة: mgr 3 (هناك قيمة واحدة فقط معدومة في العمود managerID) يمكنك أيضًا استخدام DISTINCT داخل دالة أخرى (مثل COUNT) لتجبنّب إعادة العناصر المكرّرة على النحو التالي: SELECT COUNT(ContinentCode) AllCount , COUNT(DISTINCT ContinentCode) SingleCount FROM Countries; ستعيد الشيفرة أعلاه قيمًا مختلفة. إذ لن تحسب SingleCount إلا عدد القارّات الفريدة (أي غير المكررة)، وذلك على خلاف AllCount التي ستعيد التكرارات أيضًا. إذا طبّقنا الشيفرة أعلاه على جدول القارات التالي: ContinentCode OC EU AS NA NA AF AF فسنحصل على الخرج التالي: AllCount: 7 SingleCount: 5 القيمة الدنيا Min تبحث الدالة Min عن أصغر قيمة في العمود: select min(age) from employee; سيعيد المثال أعلاه أصغر قيمة في العمود ‎age‎ من جدول ‎employee‎. القيمة القصوى Max تبحث الدالة Max عن القيمة القصوى في العمود: select max(age) from employee; سيعيد المثال أعلاه أكبر قيمة في العمود ‎age‎ من جدول ‎employee‎. الدوال العددية والصفّية Scalar/Single Row Functions توفّر SQL العديد من الدوال العددية (scalar functions) المُضمّنة. والتي تأخذ قيمة واحدة كمُدخل، وتعيد قيمة واحدة لكل صفّ في مجموعة النتائج. يمكنك استخدام الدوال العددية في أيّ موضع تكون التعابير جائزة فيه داخل ‏‏‏‏عبارات T-SQL . التاريخ والوقت في SQL، يُستخدم النوعان date و time لتخزين المعلومات المتعلقة بالوقت. يتضمّن هذان النوعان الوقت (time) والتاريخ (date) والتوقيت الصغير (smalldatetime) والتوقيت (datetime) والتوقيت 2 - مبني على 24 ساعة - (datetime2) والتوقيت الإزاحي - أي فارق التوقيت مع التوقيت العالمي الموحد UTC‏ - (datetimeoffset). لكل واحد من هذه الأنواع تنسيق خاص كما يوضّح الجدول التالي: نوع البيانات التنسيق time hh:mm:ss[.nnnnnnn] date YYYY-MM-DD smalldatetime YYYY-MM-DD hh:mm:ss datetime YYYY-MM-DD hh:mm:ss[.nnn] datetime2 YYYY-MM-DD hh:mm:ss[.nnnnnnn] datetimeoffset YYYY-MM-DD hh:mm:ss[.nnnnnnn] [+/-]hh:mm تعيد الدالة ‎DATENAME‎ اسم أو جزء محدّد من قيمة التاريخ. SELECT DATENAME (weekday,'2017-01-14') as Datename الخرج الناتج عن الشيفرة أعلاه: Datename Saturday يمكنك استخدام الدالة ‎GETDATE‎ لتحديد التاريخ والوقت الحاليين لجهاز الكمبيوتر الذي ينفّذ شيفرة SQL الحالية كما هو موضّح في المثال التالي (لا تشمل هذه الدالة اختلاف المنطقة الزمنية.) SELECT GETDATE() as Systemdate الخرج الناتج: Systemdate 2017-01-14 11:11:47.7230728 تعيد الدالة ‎DATEDIFF‎ الفرق بين تاريخين. ويحدد المعامل الأوّل الممرّر إلى هذه الدالة الجزء الذي تريد استخدامه من التاريخ لحساب الاختلاف. يمكن أن يساوي: year أو month أو week أو day أو hour أو minute أو second أو millisecond. يحدّد المعامل الثاني والثالث تاريخ البداية وتاريخ الانتهاء اللذين تريد حساب الفرق الزمني بينها على التوالي. إليك المثال التالي: SELECT SalesOrderID, DATEDIFF(day, OrderDate, ShipDate) AS 'Processing time' FROM Sales.SalesOrderHeader الخرج الناتج: SalesOrderID Processing time 43659 7 43660 7 43661 7 43662 7 تتيح لك الدالة ‎DATEADD‎ إضافة مجال زمني إلى جزء محدّد من التاريخ كما يوضّح المثال التالي: SELECT DATEADD (day, 20, '2017-01-14') AS Added20MoreDays الخرج الناتج: Added20MoreDays 2017-02-03 00:00:00.000 التعديلات على الحروف Character modifications توفّر SQL بعض الدوال التي يمكنها معالجة الأحرفِ، مثلا، يمكن تحويل الأحرف إلى أحرف كبيرة أو صغيرة، أو تحويل الأرقام إلى أرقام منسّقة تنسيقًا خاصًّا. تحوّل الدالة ‎lower(char)‎ الأحرف المُمرّرة إليها إلى أحرف صغيرة. SELECT customer_id, lower(customer_last_name) FROM customer; يعيد الاستعلام أعلاه الاسم الأخير صغيرًا، أي يحوّل SMITH إلى smith. دوال الإعدادات والتحويل الدالة ‎@@SERVERNAME‎ هي إحدى أمثلة دوال الإعدادات في SQL. توفّر هذه الدالة اسم الخادم المحلي الذي ينفّذ تعليمات SQL. SELECT @@SERVERNAME AS 'Server' الناتج: Server SQL064 في SQL، تحدث معظم عمليات تحويلات البيانات ضمنيًا، ودون أيّ تدخل من المستخدم. إن أردت تنفيذ عملية تحويل لا يمكن إجراؤها ضمنيًا، فيمكنك استخدام الدالتين ‎CAST‎ أو ‎CONVERT‎. صياغة ‎CAST‎ أبسط من صياغة ‎CONVERT‎، بيْد أنّ إمكانياتها محدودة. سنستخدم في المثال التالي كلا الدالتين ‎CAST‎ و ‎CONVERT‎ لتحويل نوع بيانات الوقت (datetime) إلى النوع ‎varchar‎. تستخدم الدالة ‎CAST‎ دائمًا التنسيق الافتراضي. على سبيل المثال، تُمثّل التواريخ والأوقات بالتنسيق YYYY-MM-DD. بالمقابل، تستخدم الدالة ‎CONVERT‎ تنسيق التاريخ والوقت الذي تحدّده أنت. سنختار في المثال التالي التنسيق 3، والذي يمثّل التنسيق dd / mm / yy. USE AdventureWorks2012 GO SELECT FirstName + ' ' + LastName + ' was hired on ' + CAST(HireDate AS varchar(20)) AS 'Cast', FirstName + ' ' + LastName + ' was hired on ' + CONVERT(varchar, HireDate, 3) AS 'Convert' FROM Person.Person AS p JOIN HumanResources.Employee AS e ON p.BusinessEntityID = e.BusinessEntityID GO ستحصل على الخرج التالي: Cast Convert David Hamiltion was hired on 2003-02-04 David Hamiltion was hired on 04/02/03 هناك مثال آخر على دوال التحويل، وهي الدالة ‎PARSE‎. تحوّل هذه الدالة سلسلة نصية إلى نوع بيانات آخر. في صياغة الدالة، عليك تحديد السلسلة النصية التي ترغب في تحويلها متبوعة بالكلمة المفتاحية ‎AS‎، ثمّ تكتب نوع البيانات المطلوب. اختياريًا، يمكنك أيضًا تحديد الإعداد الثقافي، والذي يحدّد تنسيق السلسلة النصية. في حال لم تحدّده، فستُستخدم لغة الجلسة. إذا تعذّر تحويل السلسلة النصية إلى تنسيق عددي أو تاريخ أو وقت ، فسيُطرَح خطأ. وسيتعيّن عليك حينئِذ استخدام ‎CAST‎ أو ‎CONVERT‎ لإجراء عملية التحويل. SELECT PARSE('Monday, 13 August 2012' AS datetime2 USING 'en-US') AS 'Date in English' الخرج التالي: Date in English 2012-08-13 00:00:00.0000000 الدوال المنطقية والرياضية تقدّم SQL دالتين منطقيتين، وهما CHOOSE و IIF. تعيد الدالة ‎CHOOSE‎ عنصرًا من قائمة من القيم استنادًا إلى فهرسه في القائمة. ينبغي أن يكون المعامل الأول، الذي يمثل الفهرس، عددًا صحيحًا. المعاملات التالية تحدّد قيم القائمة. في المثال التالي، سنستخدم الدالة ‎CHOOSE‎ لإعادة المُدخَل الثاني في قائمة الإدارات. SELECT CHOOSE(2, 'Human Resources', 'Sales', 'Admin', 'Marketing' ) AS Result; النتيجة: Result Sales تعيد الدالة ‎IIF‎ القيمة true إن تحقّق شرطها، خلاف ذلك، تُعيد القيمة false. في صياغة عبارة الشرط، يحدّد معامل التعبير الشرطي (booleanexpression) التعبير المنطقي. فيما يحدّد المعامل الثاني (truevalue) القيمة التي يجب إعادتها إذا لم يتحقّق الشرط، ويحدّد المعامل الثالث (false_value) القيمة التي يجب أن تُعاد خلاف ذلك. يستخدم المثال التالي الدالة IIF لإعادة إحدى قيمتين. إذا كانت مبيعات الموظف السنوية تتجاوز 200000، فسيكون ذلك الموظف مؤهّلاً للحصول على مكافأة. خلاف ذلك لن يكون مؤهّلا للحصول على مكافأة. SELECT BusinessEntityID, SalesYTD, IIF(SalesYTD > 200000, 'Bonus', 'No Bonus') AS 'Bonus?' FROM Sales.SalesPerson GO هذا هو الناتج: BusinessEntityID SalesYTD Bonus? 274 559697.5639 Bonus 275 3763178.1787 Bonus 285 172524.4512 No Bonus تتضمّن SQL العديد من الدوال الرياضية التي يمكنك استخدامها لإجراء عمليات حسابية على المُدخلات ثمّ إعادة نتائج عددية. أحد أمثلة ذلك هي الدالة ‎SIGN‎، والتي تُعيد قيمة تمثّل إشارة التعبير. إذ تشير القيمة ‎‎-1 إلى تعبير سلبي، فيما تشير القيمة ‎‎+1 إلى تعبير موجب ، أمّا 0 فيشير إلى الصفر! في المثال التالي، القيمة المُدخلة هي عدد سالب، لذا تُعاد ‎‎النتيجة ‎‎-1. SELECT SIGN(-20) AS 'Sign' الناتج: Sign -1 هناك دالة رياضية أخرى، وهي الدالة ‎POWER‎. والتي تحسب أسّ تعبير مرفوع إلى قوة محددة. في صياغة الدالة، يحدّد المعامل الأول التعبير العددي، فيما يحدّد المعامل الثاني الأسّ. SELECT POWER(50, 3) AS Result النتيجة: Result 125000 الدوال التحليلية تُستخدم الدوال التحليلية لحساب قيمة معيّنة بناءً على مجموعة من القيم. على سبيل المثال، يمكنك استخدام الدوال التحليلية لحساب المجاميع الجارية (running totals)، أو النسب المئوية، أو النتيجة الأكبر داخل مجموعة. LAG و LEAD توفر الدالة ‎LAG‎ البيانات الخاصّة بالصفوف التي تسبق الصف الحالي في مجموعة النتائج. على سبيل المثال ، في عبارة ‎SELECT‎، يمكنك موازنة قيم الصف الحالي مع قيم الصف السابق. يمكنك استخدام تعبير عددي لتحديد القيم التي يجب موازنتها. يمثّل معامل الإزاحة (offset) عدد الصفوف السابقة للصف الحالي التي ستُستخدم في المقارنة. في حال عدم تحديده، فستُستخدم القيمة الافتراضية 1. يحدّد المعامل الافتراضي default القيمة التي يجب إعادتها عندما يكون التعبير الموجود في الموضع offset معدومًا (‎NULL‎). إذا لم تحدّد قيمة لهذا المعامل، فستُستخدم القيمة الافتراضية ‎NULL‎. توفّر الدالة ‎LEAD‎ بيانات عن الصفوف التي تعقُب الصفّ الحالي في مجموعة الصفوف. على سبيل المثال، في عبارة ‎SELECT‎، يمكنك موازنة قيم الصف الحالي مع قيم الصف اللاحق. يمكن تحديد القيم التي يجب موازنتها باستخدام تعبير رقمي. يمثّل معامل الإزاحة (offset) عدد الصفوف اللاحقة للصف الحالي التي ستُستخدم في المقارنة. يحدد المعامل default القيمة التي ينبغي أن تُعاد عندما يكون التعبير الموجود عند موضع الإزاحة معدومًا (‎NULL‎). إذا لم تحدد هذين المعاملين، فستُستخدم القيمتان الافتراضيتان لهذين المعاملين، واللتان تساويان 1 و ‎NULL‎ على التوالي. يستخدم المثال التالي الدالتين LEAD و LAG لمقارنة قيم المبيعات الحالية لكل موظف مع قيم الموظفين المذكورين قبله وبعده، مع ترتيب السجلات بناءً على قيمة العمود BusinessEntityID. SELECT BusinessEntityID, SalesYTD, LEAD(SalesYTD, 1, 0) OVER(ORDER BY BusinessEntityID) AS "Lead value", LAG(SalesYTD, 1, 0) OVER(ORDER BY BusinessEntityID) AS "Lag value" FROM SalesPerson; الخرج الناتج: BusinessEntityID SalesYTD Lead value Lag value 274 559697.5639 3763178.1787 0.0000 275 3763178.1787 4251368.5497 559697.5639 276 4251368.5497 3189418.3662 3763178.1787 277 3189418.3662 1453719.4653 4251368.5497 278 1453719.4653 2315185.6110 3189418.3662 279 2315185.6110 1352577.1325 1453719.4653 PERCENTILEDISC و PERCENTILECONT تسرد الدالة ‎PERCENTILE_DISC‎ قيمة أوّل مُدخَل يكون التوزيع التراكمي (cumulative distribution) عنده أعلى من المئين الذي قدّمته باستخدام المعامل ‎numeric_literal‎. تُجمَّع القيم حسب مجموعة الصفوف (rowset) أو حسب التوزيع (partition) كما هو محدّد في عبارة ‎WITHIN GROUP‎. تشبه ‎PERCENTILE_CONT‎ الدالة ‎PERCENTILE_DISC‎، بيْد أنّها تُعيد متوسّط مجموع أول مُدخل يحقق الشرط مع المُدخل التالي. SELECT BusinessEntityID, JobTitle, SickLeaveHours, CUME_DIST() OVER(PARTITION BY JobTitle ORDER BY SickLeaveHours ASC) AS "Cumulative Distribution", PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY SickLeaveHours) OVER(PARTITION BY JobTitle) AS "Percentile Discreet" FROM Employee; لإيجاد القيمة التي تطابق أو تتجاوز المئين 0.5، عليك تمرير المئين كقيمة عددية حرفية (numeric literal) إلى دالة المئين الكسري ‎PERCENTILE_DISC‎. ينتج عن تطبيق هذه الدالة على مجموعة النتائج قائمة مؤلفة من قيم الصف التي يكون التوزيع التراكمي عندها أعلى من المئين المحدّد. BusinessEntityID JobTitle SickLeaveHours Cumulative Distribution Percentile Discreet 272 Application Specialist 55 0.25 56 268 Application Specialist 56 0.75 56 269 Application Specialist 56 0.75 56 267 Application Specialist 57 1 56 يمكنك أيضًا استخدام دالة المئين المتصل - Percentile Continuous‏ - ‎PERCENTILE_CONT‎، والتي ينتج عن تطبيقها على مجموعة النتائج متوسط مجموع قيمة النتيجة مع أعلى قيمة موالية تحقق الشرط. SELECT BusinessEntityID, JobTitle, SickLeaveHours, CUME_DIST() OVER(PARTITION BY JobTitle ORDER BY SickLeaveHours ASC) AS "Cumulative Distribution", PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY SickLeaveHours) OVER(PARTITION BY JobTitle) AS "Percentile Discreet", PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY SickLeaveHours) OVER(PARTITION BY JobTitle) AS "Percentile Continuous" FROM Employee; الخرج الناتج: BusinessEntityID JobTitle SickLeaveHours Cumulative Distribution Percentile Discreet Percentile Continuous 272 Application Specialist 55 0.25 56 56 268 Application Specialist 56 0.75 56 56 269 Application Specialist 56 0.75 56 56 267 Application Specialist 57 1 56 56 FIRST_VALUE يمكنك استخدام الدالة ‎FIRST_VALUE‎ لتحديد القيمة الأولى في مجموعة نتائج مرتّبة: SELECT StateProvinceID, Name, TaxRate, FIRST_VALUE(StateProvinceID) OVER(ORDER BY TaxRate ASC) AS FirstValue FROM SalesTaxRate; في هذا المثال، تُستخدم الدالة ‎FIRST_VALUE‎ لإعادة قيمة الحقل ‎ID‎ الخاص بالولاية أو المقاطعة التي لها أدنى معدّل للضريبة. فيما تُستخدم العبارة ‎OVER‎ لترتيب معدّلات الضريبة للحصول على أدنى معدّل. إليك جدول الضرائب: StateProvinceID Name TaxRate FirstValue 74 Utah State Sales Tax 5.00 74 36 Minnesota State Sales Tax 6.75 74 30 Massachusetts State Sales Tax 7.00 74 1 Canadian GST 7.00 74 57 Canadian GST 7.00 74 63 Canadian GST 7.00 74 LAST_VALUE تعيد الدالة ‎LAST_VALUE‎ القيمة الأخيرة في مجموعة نتائج مرتبة. SELECT TerritoryID, StartDate, BusinessentityID, LAST_VALUE(BusinessentityID) OVER(ORDER BY TerritoryID) AS LastValue FROM SalesTerritoryHistory; يستخدم المثال أعلاه الدالة ‎LAST_VALUE‎ لإعادة القيمة الأخيرة لكل مجموعة من الصفوف في مجموعة القيم المُرتبة. TerritoryID StartDate BusinessentityID LastValue 1 2005-07-01 00.00.00.000 280 283 1 2006-11-01 00.00.00.000 284 283 1 2005-07-01 00.00.00.000 283 283 2 2007-01-01 00.00.00.000 277 275 2 2005-07-01 00.00.00.000 275 275 3 2007-01-01 00.00.00.000 275 277 PERCENTRANK و CUMEDIST تحسب الدالة ‎PERCENT_RANK‎ ترتيب الصفّ بالنسبة لمجموعة الصفوف. تُحسب النسبة المئوية نسبةً إلى عدد الصفوف في المجموعة التي تقلّ قيمتها عن الصف الحالي. تُعطى للقيمة الأولى في مجموعة النتائج دائمًا النسبة المئوية 0. بالمقابل، فالنسبة المئوية للقيمة العليا - أو الأخيرة - في المجموعة تساوي دائمًا 1. تحسب الدالة ‎CUME_DIST‎ الموضع النسبي (relative position) لقيمة معيَّنة في مجموعة من القيم من خلال تحديد النسبة المئوية للقيم التي تصغُر أو تساوي تلك القيمة. تُسمّى هذه العملية التوزيع التراكمي (cumulative distribution). سنستخدم في هذا المثال عبارة ‎ORDER‎ لتقسيم - أو تصنيف - الصفوف التي أعَادتها العبارة ‎SELECT‎ بناءً على المسمّيات الوظيفية للموظّفين، مع ترتيب النتائج في كل مجموعة على أساس عدد ساعات الإجازات المرضية التي استخدمها الموظفون. SELECT BusinessEntityID, JobTitle, SickLeaveHours, PERCENT_RANK() OVER(PARTITION BY JobTitle ORDER BY SickLeaveHours DESC) AS "Percent Rank", CUME_DIST() OVER(PARTITION BY JobTitle ORDER BY SickLeaveHours DESC) AS "Cumulative Distribution" FROM Employee; الخرج الناتج: BusinessEntityID JobTitle SickLeaveHours Percent Rank Cumulative Distribution 267 Application Specialist 57 0 0.25 268 Application Specialist 56 0.333333333333333 0.75 269 Application Specialist 56 0.333333333333333 0.75 272 Application Specialist 55 1 1 262 Assitant to the Cheif Financial Officer 48 0 1 239 Benefits Specialist 45 0 1 252 Buyer 50 0 0.111111111111111 251 Buyer 49 0.125 0.333333333333333 256 Buyer 49 0.125 0.333333333333333 253 Buyer 48 0.375 0.555555555555555 254 Buyer 48 0.375 0.555555555555555 ترتّب الدالة ‎PERCENT_RANK‎ المُدخلات في كل مجموعة. فمقابل كل مُدخل، تحسب النسبة المئوية للمدخلات الأخرى في المجموعة التي لها قيم أصغر من المُدخل الممرّر. الدالة ‎CUME_DIST‎ مشابهة للدالة السايقة، بيْد أنّها تُعيد النسبة المئوية للقيم التي تصغُر القيمة الحالية أو تساويها. دوال النافذة Window Functions التحقق من وجود قيم مكررة في عمود لنفترض أن لدينا جدول البيانات التالي: id example unique_tag 1 example unique_tag 2 foo simple 42 bar simple 3 baz hello 51 quux world يعيد المثال التالي كل هذه الصفوف مع راية تحدّد ما إذا كان الوسم tag مُستخدمًا من قبل صفّ آخر. SELECT id, name, tag, COUNT(*) OVER (PARTITION BY tag) > 1 AS flag FROM items سنحصل على الخرج التالي: id name tag flag 1 example unique_tag false 2 foo simple true 42 bar simple true 3 baz hello false 51 quux world false في حالة لم تكن قاعدة بياناتك تدعم OVER و PARTITION، فيمكنك استخدام الشيفرة التالية للحصول على النتيجة نفسها: SELECT id, name, tag, (SELECT COUNT(tag) FROM items B WHERE tag = A.tag) > 1 AS flag FROM items A إيجاد السجلات الخارجة عن التسلسل باستخدام الدالة LAG إليك الجدول التالي: ID STATUS STATUS_TIME STATUS_BY 1 ONE 2016-09-28-19.47.52.501398 USER_1 3 ONE 2016-09-28-19.47.52.501511 USER_2 1 THREE 2016-09-28-19.47.52.501517 USER_3 3 TWO 2016-09-28-19.47.52.501521 USER_2 3 THREE 2016-09-28-19.47.52.501524 USER_4 يجب أن تُرتب العناصر بحسب قيمة الحقل ‎STATUS‎، بداية من القيمة "ONE" ثمّ "TWO" ثمّ "THREE". لاحظ أنّ التسلسل في الجدول غير مرتب، إذ أنّ هناك انتقالًا فوريًا من "ONE" إلى "THREE". عليك إيجاد طريقة للعثور على المستخدمين (‎STATUS_BY‎) الخارجين عن الترتيب. تساعد الدالة التحليلية ‎LAG()‎ في حل هذه المشكلة، إذ تعيد لكل صفّ، قيمة الصف السابق له: SELECT * FROM ( SELECT t.*, LAG(status) OVER (PARTITION BY id ORDER BY status_time) AS prev_status FROM test t ) t1 WHERE status = 'THREE' AND prev_status != 'TWO' في حالة لم تكن قاعدة بياناتك تدعم LAG، يمكنك استخدام الشيفرة التالية للحصول على النتيجة نفسها: SELECT A.id, A.status, B.status as prev_status, A.status_time, B.status_time as prev_status_time FROM Data A, Data B WHERE A.id = B.id AND B.status_time = (SELECT MAX(status_time) FROM Data where status_time < A.status_time and id = A.id) AND A.status = 'THREE' AND NOT B.status = 'TWO' حساب المجموع الجاري running total إليك جدول البيانات التالي: date amount 2016-03-12 200 2016-03-11 -50 2016-03-14 100 2016-03-15 100 2016-03-10 -250 بحسب المثال التالي المجموع الجاري للعمود amount في الجدول أعلاه: SELECT date, amount, SUM(amount) OVER (ORDER BY date ASC) AS running FROM operations ORDER BY date ASC الخرج الناتج: date amount running 2016-03-10 -250 -250 2016-03-11 -50 -300 2016-03-12 200 -100 2016-03-14 100 0 2016-03-15 100 -100 إضافة إجمالي الصفوف المُختارة لكل صف يضيف المثال التالي إجمالي الصفوف المختارة لكل صف: SELECT your_columns, COUNT(*) OVER() as Ttl_Rows FROM your_data_set id name Ttl_Rows 1 example 5 2 foo 5 3 bar 5 4 baz 5 5 quux 5 بدلاً من استخدام استعلامين، الأول للحصول على المجموع، والثاني للحصول على الصفّ، يمكنك استخدام التجميع - aggregate - كدالة نافذة (window function) واستخدام مجموعة النتائج الكاملة كنافذة (window). يمكن أن يجنّبك هذا تعقيدات عمليات الضمّ الذاتي (self joins) الإضافية. الحصول على أحدث N صفًّا في عدة مجموعات إليك البيانات التالية: User_ID Completion_Date 1 2016-07-20 1 2016-07-21 2 2016-07-20 2 2016-07-21 2 2016-07-22 إن استخدمت القيمة n = 1 في المثال التالي، ستحصل على أحدث صفّ لكل معرِّف ‎user_id‎: ;with CTE as (SELECT *, ROW_NUMBER() OVER (PARTITION BY User_ID ORDER BY Completion_Date DESC) Row_Num FROM Data) SELECT * FORM CTE WHERE Row_Num <= n الخرج سيكون: User_ID Completion_Date Row_Num 1 2016-07-21 1 2 2016-07-22 1 ترجمة -وبتصرّف- للفصول من 42 إلى 45 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: دوال التعامل مع النصوص في SQL المقال السابق: مواضيع متقدمة في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  19. ستعرض هذه المقالة عددًا من المواضيع المتقدمة في SQL، مثل إدارة الصلاحيات، واستخدام ملفات XML في الاستعلامات، والمفاتيح الرئيسية والفهارس وأرقام الصفوف. GRANT و REVOKE تُستخدم العبارة GRANT لمنح الإذن لمستخدم ما بإجراء عملية على قاعدة البيانات، فيما تُستخدم العبارة REVOKE لسحب الإذن أو الصلاحية منه. يمنح المثال التالي الإذن للمستخدمَين ‎User1‎ و ‎User2‎ بإجراء العمليتين ‎SELECT‎ و ‎UPDATE‎ على الجدول ‎Employees‎. GRANT SELECT, UPDATE ON Employees TO User1, User2; يسحب المثال التالي من المستخدمَين ‎User1‎ و ‎User2‎ صلاحية تنفيذ العمليتين ‎SELECT‎ و ‎UPDATE‎ على جدول الموظفين. REVOKE SELECT, UPDATE ON Employees FROM User1, User2; استخدام ملفات XML في SQL يمكن تعريف جدول بيانات من ملف XML واستخدامه في استعلامات SQL مثل أيّ جدول عادي: DECLARE @xmlIN XML = '<TableData> <aaa Main="First"> <row name="a" value="1" /> <row name="b" value="2" /> <row name="c" value="3" /> </aaa> <aaa Main="Second"> <row name="a" value="3" /> <row name="b" value="4" /> <row name="c" value="5" /> </aaa> <aaa Main="Third"> <row name="a" value="10" /> <row name="b" value="20" /> <row name="c" value="30" /> </aaa> </TableData>' SELECT t.col.value('../@Main', 'varchar(10)') [Header], t.col.value('@name', 'VARCHAR(25)') [name], t.col.value('@value', 'VARCHAR(25)') [Value] FROM @xmlIn.nodes('//TableData/aaa/row') AS t (col) الخرج الناتج: Header name Value First a 1 First b 2 First c 3 Second a 3 Second b 4 Second c 5 Third a 10 Third b 20 Third c 30 المفاتيح الرئيسية Primary Keys تُستخدَم المفاتيح الرئيسية لتمييز صفوف جدول من قاعدة بيانات. ولا يُسمح إلا بمفتاح رئيسي واحد في كلّ جدول. تنشئ الشيفرة التالية جدولًا للموظفين، مع جعل المُعرّف Id مفتاحه الرئيسي: CREATE TABLE Employees ( Id int NOT NULL, PRIMARY KEY (Id), ... ); يمكن أيضًا تركيب مفتاح من حقل واحد أو أكثر، هذا النوع من المفاتيح يُسمّى المفاتيح المُركّبة، وتُصاغ على النحو التالي: CREATE TABLE EMPLOYEE ( e1_id INT, e2_id INT, PRIMARY KEY (e1_id, e2_id) ) استخدام الزيادة التلقائية Auto Increment تسمح العديد من قواعد البيانات بزيادة قيمة المفتاح الرئيسي تلقائيًا عند إضافة مفتاح جديد. يضمن هذا السلوك أن تكون كلّ المفاتيح مختلفة عن بعضها. إليك الأمثلة التوضيحية التالية: MySQL CREATE TABLE Employees ( Id int NOT NULL AUTO_INCREMENT, PRIMARY KEY (Id) ); PostgreSQL CREATE TABLE Employees ( Id SERIAL PRIMARY KEY ); SQL Server CREATE TABLE Employees ( Id int NOT NULL IDENTITY, PRIMARY KEY (Id) ); SQLite CREATE TABLE Employees ( Id INTEGER PRIMARY KEY ); الفهارس Indexes الفهارس هي بنيات تحتوي على مؤشّرات تشير إلى محتويات جدول مُرتّب ترتيبًا محدّدًا، تُستخدم الفهارس لتحسين أداء الاستعلامات. فهي تشبه فهرس الكتاب، حيث تُفهرَس الصفحات (صفوف الجدول) بأرقام مميّزة تسهّل الوصول إليها. توجد عدّة أنواع من الفهارس، ويمكنك إنشاؤها على أيّ جدول. يحسّن استخدَام فهارس الأعمدة في عبارات WHERE أو JOIN أو ORDER BY أداء الاستعلامات تحسينًا. الفهارس المُرتّبة Sorted Index إذا كانت الفهارس مُرتّبة بنفس الترتيب الذي ستُسترجع به، فلن تجري التعليمة ‎SELECT‎ أيّ ترتيب إضافي أثناء الاسترجاع. CREATE INDEX ix_scoreboard_score ON scoreboard (score DESC); عند تنفيذ الاستعلام التالي: SELECT * FROM scoreboard ORDER BY score DESC; لن يُجري نظام قاعدة البيانات أيّ ترتيب إضافي طالما أنّه سيبحث في الفهرس وفق ذلك الترتيب. الفهرس الجزئي أو المُصفّى Partial or Filtered Index تتيح SQL Server و SQLite إنشاء فهارس تحتوي جزءًا من الأعمدة، وكذلك جزءًا من الصفوف. لنعتبر كمّية متزايدة من الطلبات يساوي الحقل ‎order_state_id‎ الخاص بها القيمة 2 (والتي تمثّل الحالة "مكتملة")، وكمّية أخرى ثابتة من الطلبات يساوي الحقل ‎order_state_id الخاص بها القيمة 1 (والتي تمثّل الحالة "غير مكتملة"). إليك الاستعلام التالي: SELECT id, comment FROM orders WHERE order_state_id = 1 AND product_id = @some_value; يتيح لك استخدام الفهرسة الجزئية تقييد الفهرس (limit the index)، بحيث لا تُضمَّن إلا الطلبات التي لم تكتمل بعدُ: CREATE INDEX Started_Orders ON orders(product_id) WHERE order_state_id = 1; سيؤدي هذا إلى تقليل كمّية المؤشّرات، ويوفّر مساحة التخزين، ويقلّل من تكلفة تحديث الفهارس. إنشاء فهرس تنشئ الشيفرة التالية فهرسًا للعمود EmployeeId في الجدول Cars. CREATE INDEX ix_cars_employee_id ON Cars (EmployeeId); يحسّن استخدام الفهرس سرعة الاستعلامات التي تحاول أن ترتّب أو تختار الصفوف وفقًا لقيم EmployeeId، كما في المثال التالي: SELECT * FROM Cars WHERE EmployeeId = 1 يمكن أن يحتوي الفهرس أكثر من عمود واحد كما في المثال التالي: CREATE INDEX ix_cars_e_c_o_ids ON Cars (EmployeeId, CarId, OwnerId); في هذه الحالة، سيكون الفهرس مفيدًا للاستعلامات التي تحاول أن ترتّب أو تختار السجلات وفقًا لجميع الأعمدة المُضمّنة في حال كانت مجموعة الشروط مُرتّبة بالطريقة نفسها. هذا يعني أنه عند استرداد البيانات، سيكون بمقدورك العثور على الصفوف المُراد إعادتها باستخدام الفهرس بدلًا من البحث في كامل الجدول. يستخدم المثال التالي الفهرس الثاني: SELECT * FROM Cars WHERE EmployeeId = 1 Order by CarId DESC يفقد الفهرس هذه المزايا في حال كان الترتيب مختلفًا كما يبيّن المثال التالي: SELECT * FROM Cars WHERE OwnerId = 17 Order by CarId DESC لم يعد الفهرس مفيدًا الآن، لأنّه يتوجّب على قاعدة البيانات أن تسترجع الفهرس بالكامل، وعبر جميع قيم EmployeeId و CarID بُغية إيجاد العناصر التي تحقق الشرط ‎OwnerId = 17‎. رغم كل ما قلناه، إلا أنّه من الممكن أن يُستخدم الفهرس رغم كل شيء؛ فقد يخمّن محسّن الاستعلامات (query optimizer) أنّ استرداد الفهرس، والتصفية بحسب قيمة ‎OwnerId‎، ثم استرداد الصفوف المطلوبة حصرًا، سيكون أسرع من استرداد الجدول بالكامل ، خاصةً إن كان الجدول كبيرًا. محو فهرس أو تعطيله وإعادة إنشائه تُستخدم التعليمة ‎DROP‎ لحذف الفهرس. في هذا المثال، تمحو ‎DROP‎ فهرسًا يُسمّى ix_cars_employee_id في الجدول Cars: DROP INDEX ix_cars_employee_id ON Cars; تحذف ‎DROP‎ الفهرس نهائيًا، وفي حال كان الفهرس مُجمّعًا (clustered)، فسيُزال التجميع، ولن يكون بالإمكان إعادة بنائه دون إعادة إنشاء الفهرس، وهي عمليّة يمكن أن تكون بطيئة ومكلفة من الناحية الحسابية. هناك حلّ آخر، وهو تعطيل (disable) الفهرس بدل حذفه: ALTER INDEX ix_cars_employee_id ON Cars DISABLE; يسمح هذا للجدول بالاحتفاظ ببنية الفهرس وبياناته الوصفية (metadata). إضافة إلى إحصائيات الفهرس، وهكذا سيسهُل تقييم التغيير الحاصل. وإذا لزم الأمر، سيكون من الممكن إعادة بناء الفهرس لاحقًا دون الاضطرار إلى إعادة إنشائه من الصفر: ALTER INDEX ix_cars_employee_id ON Cars REBUILD; الفهارس المجمّعة Clustered أو الفريدة Unique أو المُرتّبة Sorted لكل فهرس عدد من الخصائص، هذه الخصائص يمكن أن تُحدّد ساعة إنشاء الفهرس، أو يمكن أن تعيَّن لاحقًا. CREATE CLUSTERED INDEX ix_clust_employee_id ON Employees(EmployeeId, Email); تنشئ عبارة SQL أعلاه فهرسًا مُجمّعا (clustered index) جديدًا لجَدول الموظفين Employees. الفهارس المُجمّعة هي فهارس تتحكّم في البنية الفعلية للجدول؛ إذ يُرتَّب الجدول بحيث يُطابق بنية الفهرس. نتيجة لهذا، لا يمكن أن يكون للجدول أكثر من فهرس مُجمّع واحد. ما يعني أنّ الشيفرة أعلاه ستفشل في حال كان الجدول يتوفّر سلفًا على فهرس مُجمّع (تسمّى الجداول التي لا تحتوي على فهارس مُجمّعة "كوْمَات" heaps). ينشئ المثال التالي فهرسًا فريدًا (unique index) للعمود Email في جدول العملاء Customers. علاوة على تسريع الاستعلام، يفرض الفهرس أن تكون عناوين البريد الإلكتروني فريدة (غير مكرّرة) في العمود. وإن حاولت إدراج صفّ يحتوي بريدًا إلكترونيًا موجودًا سلفا، فستفشل عملية الإدراج أو التحديث (افتراضيًا). CREATE UNIQUE INDEX uq_customers_email ON Customers(Email); ينشئ المثال التالي فهرسًا لجدول العملاء Customers، هذا الفهرس يضع على الجدول قيودًا تنصّ على أنّ الحقل EmployeeID ينبغي أن يكون فريدًا. (ستفشل هذه العملية إن لم يكن العمود فريدًا - أي، إن كان هناك موظف آخر يحمل نفس القيمة.) CREATE UNIQUE INDEX ix_eid_desc ON Customers(EmployeeID); تنشئ الشيفرة التالية فهرسًا مُرتّبا ترتيبًا تنازليًا. افتراضيا، تُرتّب الفهارس (على الأقل في MSSQL server) تصاعديًا، لكن يمكن تغيير هذا السلوك كما يوضّح المثال التالي: CREATE INDEX ix_eid_desc ON Customers(EmployeeID Desc); إعادة بناء الفهرس مع مرور الوقت، قد تصبح الفهارس المتشعّبة من النمط ب B-Tree مُجزّأة (fragmented) نتيجة عمليات التحديث والحذف والإدراج. في نظام SQLServer، هناك نوعان من الفهارس، الفهارس الداخلية، والتي تكون فيها صفحة الفهرس نصف فارغة (half empty)، والفهارس الخارجية، والتي لا يتطابق فيها ترتيب الصفحة المنطقي مع الترتيب الفعلي). إعادة بناء الفهارس تشبه إلى حدّ بعيد حذفها ثمّ إعادة إنشائها. يمكن إعادة بناء الفهرس باستخدام الصياغة التالية: ALTER INDEX index_name REBUILD; إعادة بناء الفهارس هي عملية منقطعة (offline) افتراضيًا، أيّ أنّها تقفِل الجدول أثناء عملها وتمنع التعديل عليه، لكنّ العديد من أنظمة معالجة قواعد البيانات (RDBMS) تسمح بإعادة البناء عبر الشبكة (online). كما توفّر بعض أنظمة قواعد البيانات بدائل أخرى لإعادة بناء الفهارس، مثل ‎REORGANIZE‎ (في SQLServer) أو ‎COALESCE‎ / ‎SHRINK SPACE‎ (في Oracle). الإدراج باستخدام فهرس فريد ستفشل الشيفرة التالية في حال تم تعيين فهرس فريد للعمود Email في جدول العملاء: UPDATE Customers SET Email = "richard0123@example.com" WHERE id = 1; تقترح هذه الشيفرة بديلًا ممكنًا في مثل هذه الحالة: UPDATE Customers SET Email = "richard0123@example.com" WHERE id = 1 ON DUPLICATE KEY; رقم الصف Row number يمكن استخدام أرقام الصفوف في استعلامات SQL عبر الدالة ROW_NUMBER. يحذف المثال التالي جميع السجلات خلا السجلّ الأخير (جدول من نوع "واحد إلى كثير" - 1‎‎ to Many) WITH cte AS ( SELECT ProjectID, ROW_NUMBER() OVER (PARTITION BY ProjectID ORDER BY InsertDate DESC) AS rn FROM ProjectNotes ) DELETE FROM cte WHERE rn > 1; تضمّن الشيفرة التالية رقم الصف وفقًا لترتيب الطلبية. SELECT ROW_NUMBER() OVER(ORDER BY Fname ASC) AS RowNumber, Fname, LName FROM Employees يقسّم المثال التالي أرقام الصفوف إلى مجموعات وفقًا لمعيار محدّد. SELECT ROW_NUMBER() OVER(PARTITION BY DepartmentId ORDER BY DepartmentId ASC) AS RowNumber, DepartmentId, Fname, LName FROM Employees الفرق بين Group By و Distinct في SQL تُستخدم العبارة ‎GROUP ‎BY‎ في SQL مع دوال التجميع (aggregation functions). إليك الجدول التالي: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } orderId userId storeName orderValue orderDate 1 43 Store A 25 20-03-2016 2 57 Store B 50 22-03-2016 3 43 Store A 30 25-03-2016 4 82 Store C 10 26-03-2016 5 21 Store A 45 29-03-2016 يستخدم الاستعلام أدناه ‎GROUP BY‎ لإجراء عمليات حسابية تجميعية (aggregated calculations). SELECT storeName, COUNT(*) AS total_nr_orders, COUNT(DISTINCT userId) AS nr_unique_customers, AVG(orderValue) AS average_order_value, MIN(orderDate) AS first_order, MAX(orderDate) AS lastOrder FROM orders GROUP BY storeName; يُعاد الخرج التالي: storeName total_nr_orders nr_unique_customers average_order_value first_order lastOrder Store A 3 2 33.3 20-03-2016 29-03-2016 Store B 1 1 50 22-03-2016 22-03-2016 Store C 1 1 10 26-03-2016 26-03-2016 بالمقابل، تُستخدم ‎DISTINCT‎ لسرد توليفة فريدة (unique combination) من القيم المختلفة للأعمدة المحدّدة. SELECT DISTINCT storeName, userId FROM orders; سنحصل على الخرج: storeName userId Store A 43 Store B 57 Store C 82 Store A 21 البحث عن التكرارات في جزء من الأعمدة يستخدم المثال التالي دالة نافذة - Window Function - (أي دالة تجري حسابات على مجموعة من الصفوف، كما تتيح الوصول إلى بيانات السجلات التي تسبق السجل الحالي أو التي تلحقه) لعرض جميع الصفوف المُكرّرة (في جزء من الأعمدة). WITH CTE (StudentId, Fname, LName, DOB, RowCnt) as ( SELECT StudentId, FirstName, LastName, DateOfBirth as DOB, SUM(1) OVER (Partition By FirstName, LastName, DateOfBirth) as RowCnt FROM tblStudent ) SELECT * from CTE where RowCnt > 1 ORDER BY DOB, LName ترجمة -وبتصرّف- للفصول من 34 إلى 40 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: الدوال النصية String Functions في SQL المقال السابق: حذف الجداول وقواعد البيانات في SQL النسخة العربية الكاملة لكتاب ملاحظات للعاملين بلغة SQL 1.0.0
  20. تتحدّث هذه المقالة عن كيفية حذف الجداول وقواعد البيانات (DROP و DELETE)، واقتطاع الجداول (TRUNCATE TABLE)، وكيفية استخدام الحذف المتسلسل (Cascading Delete) في SQL. العبارة DELETE تُستخدَم عبارة DELETE لحذف السجلات من جدول معيّن. حذف جميع الصفوف عند استخدام DELETE بدون عبارة ‎WHERE‎، ستُحذف جميع الصفوف من الجدول. DELETE FROM Employees على العموم، أداء العبارة TRUNCATE (انظر الفقرة أدناه) أفضل من أداء DELETE، لأنّها تتجاهل الزنادات (triggers) والفهارس وتحذف البيانات مباشرة. استخدام DELETE مع WHERE ستحذف الشيفرة التالية جميع الصفوف التي تفي بشرط ‎WHERE‎، أي الصفوف التي تمثل الموظفين الذين يحملون الاسم John . DELETE FROM Employees WHERE FName = 'John' الاقتطاع عبر TRUNCATE تُستخدم عبارة الاقتطاع TRUNCATE لإعادة الجدول إلى الحالة التي كان عليها عند إنشائه. إذ تُحذف جميع الصفوف من الجدول، ويُعاد تعيين القيم تلقائية الزيادة (auto-increment) إلى قيمتها الأولى. لا تحذف TRUNCATE كلّ صف على حدة كما هو الشأن مع DELETE: TRUNCATE TABLE Employees حذف بعض الصفوف بناءً على نتائج عمليات المقارنة مع جداول أخرى من الممكن حذف البيانات (‎DELETE‎) من جدول إذا كانت مطابقة (أو غير مطابقة) لبيانات جدول آخر. لنفترض أنّنا نريد حذف البيانات من الجدول المصدري بمجرّد تحميلها إلى الجدول الهدف. DELETE FROM Source WHERE EXISTS ( SELECT 1 --ليست مهمّة SELECT القيمة المحدّدة في FROM Target Where Source.ID = Target.ID ) تسمح معظم أنظمة معالجة قواعد البيانات (RDBMS) الشهيرة (مثل MySQL و Oracle و PostgresSQL و Teradata) بضمّ الجداول خلال عملية الحذف ‎DELETE‎، ممّا يتيح إجراء موازنات معقّدة في عبارات قصيرة. دورة علوم الحاسوب دورة تدريبية متكاملة تضعك على بوابة الاحتراف في تعلم أساسيات البرمجة وعلوم الحاسوب اشترك الآن لنفترض الآن أنّنا نريد تجميع (Aggregate) جدول من الجدول الهدف على أساس التاريخ date وليس المُعرّف ID. لنفترض أيضًا أنّنا نريد ألّا تُحذف البيانات من المصدر إلّا بعد أن يُملأ حقل التاريخ Date الخاص بالجدول المُجمَّع (aggregate). في أنظمة MySQL و Oracle و Teradata، يمكن القيام بذلك باستخدام: DELETE FROM Source WHERE Source.ID = TargetSchema.Target.ID AND TargetSchema.Target.Date = AggregateSchema.Aggregate.Date أمّا في PostgreSQL، فاستخدم الصياغة التالية: DELETE FROM Source USING TargetSchema.Target, AggregateSchema.Aggregate WHERE Source.ID = TargetSchema.Target.ID AND TargetSchema.Target.DataDate = AggregateSchema.Aggregate.AggDate ينتج عن هذا أساسًا عمليات ضمّ داخلي (INNER JOINs) بين الجدول المصدري والجدول الهدف والجدول المُجمّع (Aggregate.) يُنفّذ الحذف على الجدول المصدري في حال وجود نفس المعرّفات في الهدف، وكذلك في حال تساوي التاريخين date في الجدول الهدف وكذلك في الجدول المُجمَّع. يمكن كتابة الاستعلام نفسه (في MySQL و Oracle و Teradata) على النحو التالي: DELETE Source FROM Source, TargetSchema.Target, AggregateSchema.Aggregate WHERE Source.ID = TargetSchema.Target.ID AND TargetSchema.Target.DataDate = AggregateSchema.Aggregate.AggDate في بعض أنظمة إدارة قواعد البيانات (مثل Oracle و MySQL)، يُمكن أن استخدام عمليات الضمّ joins صراحة في عبارات ‎Delete‎، بيْد أنّها غير مدعومة في جميع المنصات (كما هو الحال في Teradata). يمكن إجراء عمليات الموازنة للتحقق من سيناريوهات عدم التطابق بدلاً من سيناريوهات التطابق مع جميع أنماط الصياغات (لاحظ ‎NOT‎ EXISTS‎ أدناه): DELETE FROM Source WHERE NOT EXISTS ( SELECT 1 -- لا تهمّ SELECT القيم المحدّدة في FROM Target Where Source.ID = Target.ID ) الاقتطاع عبر TRUNCATE تحذف عبارة TRUNCATE كافة البيانات من الجدول. فهي تكافئ إجراء عملية الحذف DELETE بدون تصفية، ولكن قد تكون لها بعض القيود أو التحسينات اعتمادا على برنامج قواعد البيانات المُستخدم. يزيل المثال التالي جميع الصفوف من جدول الموظفين Employee: TRUNCATE TABLE Employee; يُفضّل عمومًا استخدام TRUNCATE على DELETE، لأنّها تتجاهل جميع الفهارس والزنادات (triggers)، وتزيل العناصر مباشرة. حذف الجداول (DELETE) هي عملية تعمل على الصفوف، بمعنى أنّها تحذف كل صفّ على حدة. أما اقتطاع الجداول، فهي عملية تعمل على صفحة كاملة من البيانات (page operation)، إذ يُعاد تخصيص (reallocate) صفحة البيانات بأكملها. إذا كان لديك جدول يحتوي مليون صفّ، فسيكون اقتطاع الجدول أسرع بكثير من استخدام عبارة حذف DELETE الجدول. بالمقابل، يمكننا تحديد الصفوف المراد حذفها باستخدام DELETE، ولكن لا يمكننا تحديد الصفوف المراد اقتطاعها، إذ لا يمكننا سوى اقتطاع جميع السجلات مرّة واحدة. يؤدّي حذف جميع الصفوف (عبر DELETE) ثم إدراج سجلات جديدة إلى زيادة قيمة المفتاح الرئيسي المتزايد تلقائيًا (Auto incremented Primary key) انطلاقًا من القيمة المُدرجة سابقًا، أمّا في عبارة Truncate، فسيُعاد تعيين قيمة المفتاح الرئيسي التلقائي، وسيبدأ من 1. لاحظ أنه عند اقتطاع جدول ما، يجب ألا تكون هناك مفاتيح خارجية (foreign keys)، وإلا فسيُطرح خطأ. DROP محو جدول DROP TABLE تحذف عبارة DROP TABLE جدولًا مع بياناته من قاعدة البيانات بشكل دائم. الأمثلة التالية تتحقّق من وجود الجدول قبل محوه: MySQL ≥ 3.19 DROP TABLE IF EXISTS MyTable; PostgreSQL ≥ 8.x DROP TABLE IF EXISTS MyTable; SQL Server ≥ 2005 If Exists(Select * From Information_Schema.Tables Where Table_Schema = 'dbo' And Table_Name = 'MyTable') Drop Table dbo.MyTable SQLite ≥ 3.0 DROP TABLE IF EXISTS MyTable; محو قاعدة بيانات يمكن محو قاعدة البيانات باستخدام عبارة DROP DATABASE. تنبيه: تحذف DROP DATABASE قاعدة البيانات نهائيًا، لذا عليك أن تحرص دائمًا على تخزين نسخة احتياطية من قاعدة البيانات إن خشيت ضياع البيانات. تمحو الشيفرة التالي قاعدة بيانات الموظفين: DROP DATABASE [dbo].[Employees] الحذف المتسلسل Cascading Delete لنفترض أنّ لديك تطبيقًا يدير فندقًا يضمّ عددًا من الغرف. لنفترض أنّ لديك العديد من العملاء، وقد قرّرت إنشاء قاعدة بيانات لتخزين المعلومات الخاصة بعملائك. ستحتوي قاعدة البيانات جدولًا واحدًا للعملاء، وآخر للغرف. كل عميل يمكن أن يستأجر N غرفة. هذا يعني أنّ جدول الغرف سيحتوي مفتاحًا خارجيًا (foreign key) يشير إلى جدول العملاء. ALTER TABLE dbo.T_Room WITH CHECK ADD CONSTRAINT FK_T_Room_T_Client FOREIGN KEY(RM_CLI_ID) REFERENCES dbo.T_Client (CLI_ID) GO عند خروج أحد العملاء، سيتعيّن عليك حذف بياناته من البرنامج. .لكن إن كتبت: DELETE FROM T_Client WHERE CLI_ID = x فسترتَكب "انتهاكَ مفتاحٍ خارجي" (foreign key violation)، ذلك أنّه لا يجوز لك حذف عميل لديه غرفة. عليك حذف غرف العميل قبل أن تحذف العميل. لنفترض أنّك تتوقّع أنّه قد تُضاف العديد من المفاتيح الخارجية (foreign key dependencies) في قاعدة البيانات مستقبلا نتيجةً لنموّ التطبيق. قد يخلق هذا مشكلة كبيرة. لأنّه في كلّ مرّة تعدّل قاعدة البيانات، سيكون عليك تعديل شيفرة تطبيقك في كل المواضِع المرتبطة بها. وقد يكون عليك أيضًا تعديل شيفرات تطبيقات أخرى (مثل الواجهات البرمجية للأنظمة الأخرى). هناك حل أفضل يكفيك كلّ هذا العناء. فيكفي أن تضيف العبارة ‎ON DELETE CASCADE‎ إلى مفتاحك الخارجي. ALTER TABLE dbo.T_Room -- WITH CHECK -- SQL-Server can specify WITH CHECK/WITH NOCHECK ADD CONSTRAINT FK_T_Room_T_Client FOREIGN KEY(RM_CLI_ID) REFERENCES dbo.T_Client (CLI_ID) ON DELETE CASCADE الآن يمكنك أن تكتب: DELETE FROM T_Client WHERE CLI_ID = x وستُحذف الغرف تلقائيًا عند حذف العميل. لقد حللنا المشكلة دون الحاجة إلى إجراء تغييرات في ِشيفرة التطبيق. تنبيه: في Microsoft SQL-Server، لن تنجح هذه المقاربة إذا كان الجدول يشير إلى نفسه. لذا إن حاولت إجراء حذف متسلسل على بنية متشعّبة عودية (recursive tree structure)، على النحو التالي: IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_T_FMS_Navigation_T_FMS_Navigation]') AND parent_object_id = OBJECT_ID(N'[dbo].[T_FMS_Navigation]')) ALTER TABLE [dbo].[T_FMS_Navigation] WITH CHECK ADD CONSTRAINT [FK_T_FMS_Navigation_T_FMS_Navigation] FOREIGN KEY([NA_NA_UID]) REFERENCES [dbo].[T_FMS_Navigation] ([NA_UID]) ON DELETE CASCADE GO IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_T_FMS_Navigation_T_FMS_Navigation]') AND parent_object_id = OBJECT_ID(N'[dbo].[T_FMS_Navigation]')) ALTER TABLE [dbo].[T_FMS_Navigation] CHECK CONSTRAINT [FK_T_FMS_Navigation_T_FMS_Navigation] GO فلن ينجح الأمر، لأنّ Microsoft-SQL-server لن تسمح لك بتعيين مفتاح خارجي باستخدام ‎ON DELETE CASCADE‎ على بنية متشعّبة عودية. أحد أسباب ذلك هو أنّ الشعبة قد تكون دورية، وهذا قد يؤدي إلى عملية سرمدية غير منتهية. نظام PostgreSQL من ناحية أخرى يمكنه القيام بذلك؛ شريطة ألّا تكون الشعبة دورية (non-cyclic). إذ أنّه في حال كانت الشعبة دورية، فسيُطرح خطأ وقت التشغيل. الحل في مثل هذه الحالة هو إنشاء دالة حذف مخصّصة. تنبيه: لا يمكنك حذف جدول العملاء وإعادة إدراج القيم مرّة أخرى، وإن حاولت ذلك، فستُحذف جميع المدخلات في الجدول T_Room. ترجمة -وبتصرّف- للفصول من 29 إلى 33 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: مواضيع متقدمة في SQL المقال السابق: معالجة الأخطاء والتعديل على قواعد البيانات في SQL النسخة العربية الكاملة لكتاب ملاحظات للعاملين بلغة SQL 1.0.0
  21. تستعرض هذه المقالة كيفية معالجة الأخطاء باستخدام العبارة TRY / CATCH، وكيفية حساب الاتحاد (UNION) وبعض العمليات الأخرى التي تمكّن من تعديل قواعد البيانات. العبارة TRY / CATCH تُستخد عبارة TRY / CATCH لإمساك الأخطاء في الشيفرات التي يُتوقع أن تطرح أخطاء. ستفشل عمليتا الإدراج في المثال التالي بسبب خطأ في صياغة التاريخ: BEGIN TRANSACTION BEGIN TRY INSERT INTO dbo.Sale(Price, SaleDate, Quantity) VALUES (5.2, GETDATE(), 1) INSERT INTO dbo.Sale(Price, SaleDate, Quantity) VALUES (5.2, 'not a date', 1) COMMIT TRANSACTION END TRY BEGIN CATCH THROW ROLLBACK TRANSACTION END CATCH في المثال التالي، ستكتمل عَمليتا الإدراج دائمًا: BEGIN TRANSACTION BEGIN TRY INSERT INTO dbo.Sale(Price, SaleDate, Quantity) VALUES (5.2, GETDATE(), 1) INSERT INTO dbo.Sale(Price, SaleDate, Quantity) VALUES (5.2, GETDATE(), 1) COMMIT TRANSACTION END TRY BEGIN CATCH THROW ROLLBACK TRANSACTION END CATCH الاتحاد UNION تُستخدم الكلمة المفتاحية UNION في SQL لدمج نتائج عبارتي SELECT دون تكرار. أي أنّها تشبه عملية الاتحاد المعروفة في علم المجموعات. من أجل استخدام UNION لدمج النتائج، يُشترط أن يكون لكلي عبارتي SELECT عدد الأعمدة ونوع البيانات نفسه، ووفق الترتيب نفسه، ولكن يجوز أن تختلف أطوال الأعمدة. إليك الجدولين التاليين: CREATE TABLE HR_EMPLOYEES ( PersonID int, LastName VARCHAR(30), FirstName VARCHAR(30), Position VARCHAR(30) ); CREATE TABLE FINANCE_EMPLOYEES ( PersonID INT, LastName VARCHAR(30), FirstName VARCHAR(30), Position VARCHAR(30) ); باستخدام ‎UNION‎، يمكننا الحصول على جميع المدراء (‎manager‎) العاملين في القسمين HR و Finance على النحو التالي: SELECT FirstName, LastName FROM HR_EMPLOYEES WHERE Position = 'manager' UNION ALL SELECT FirstName, LastName FROM FINANCE_EMPLOYEES WHERE Position = 'manager' تزيل العبارة ‎UNION‎ الصفوف المكرّرة من نتائج الاستعلام. لكن لمّا كان من الممكن أن يشترك عدّة أشخاص في نفس الاسم، ويحتلّون نفس الموقع في كلا القسمين، فسنستخدم عبارة ‎UNION ALL‎ التي لا تزيل التكرارات. يمكنك تكنِية (aliasing) الأعمدة المُعادة عبر وضع الكُنى في أول عبارات select على النحو التالي: SELECT FirstName as 'First Name', LastName as 'Last Name' FROM HR_EMPLOYEES WHERE Position = 'manager' UNION ALL SELECT FirstName, LastName FROM FINANCE_EMPLOYEES WHERE Position = 'manager' هناك فرق أساسي بين ‎UNION‎ و UNION ALL: تضمّ ‎UNION‎ مجموعتي النتائج مع إزالة التكرارات من مجموعة النتائج تضمّ UNION ALL مجموعتي النتائج دون إزالة التكرارات من الأخطاء الشائعة استخدامُ ‎UNION‎ في المواضع التي لا تكون فيها حاجة إلى إزالة التكرار من النتائج، فالكلفة الإضافية على الأداء قد تكون أكبر من المكاسب الناجمة عن إزالة التكرارات. متى تستخدم UNION لنفترض أنك تريد تصفية الدول بحسب قيم سمتين (attributes) مختلفتين، وأنّك أنشأت فهارس منفصلة غير مجمّعة (non-clustered indexes) لكل عمود. في هذه الحالة، يمكنك استخدام ‎UNION‎، التي تتيح لك استخدام كلا الفهرسين مع تجنّب التكرارات. SELECT C1, C2, C3 FROM Table1 WHERE C1 = @Param1 UNION SELECT C1, C2, C3 FROM Table1 WHERE C2 = @Param2 لن تُستخدم إلا الفهارس البسيطة في تنفيذ الاستعلامات، كما يمكنك تقليل عدد الفهارس المنفصلة غير المجمّعة (separate non-clustered indexes)، وهذا سيؤدي إلى تحسين الأداء. متى تستخدم UNION ALL لنفترض أنك تريد تصفية جدول ما بحسب قيمتي سمتين، بيْد أنّك لا تحتاج إلى تصفية السجلات المكرّرة (إمّا لأنّ ذلك لن يضرّ، أو أنّك صمّمت قاعدة البيانات بحيث لن ينتج أيّ تكرارات عن عملية الاتحاد). SELECT C1 FROM Table1 UNION ALL SELECT C1 FROM Table2 يمكن أن يكون استخدام UNION ALL مفيدًا عند إنشاء معارض (Views) تضمّ (join) بيانات صُمِّمت لكي تُقسَّم وتُوزَّع عبر عدّة جداول (ربما لأسباب تتعلق بتحسين الأداء). ولمّا كانت البيانات مقُسمة سلفًا، فإنّ جعل محرّك قاعدة البيانات يزيل التكرارات لن يضيف أيّ قيمة، وسَيبطئ الاستعلام. ALTER TABLE تُستخدم العبارة ALTER في SQL لتعدِيل قيد (constraint) أو عمود من جدول. إضافة عمود يضيف المثال التالي عمودين إلى جدول الموظفين، الأول باسم ‎StartingDate‎، ولا يمكن أن يكون معدومًا (not NULLABLE)، وقيمته الافتراضية تساوي التاريخ الحالي، أمّا العمود الثاني، فيُسمّى ‎DateOfBirth‎، ويمكن أن يكون معدومًا. ALTER TABLE Employees ADD StartingDate date NOT NULL DEFAULT GetDate(), DateOfBirth date NULL حذف عمود تحذف الشيفرَة أدناه العمود salary من جدول الموظفين: ALTER TABLE Employees DROP COLUMN salary; إضافة مفتاح رئيسي Primary Key تضيف الشيفرة أدناه مفتاحًا رئيسيًا إلى جدول "الموظفين" في الحقل ‎ID‎: ALTER TABLE EMPLOYEES ADD pk_EmployeeID PRIMARY KEY (ID) يؤدّي تضمين عدّة أسماء أعمدة بين قوسين بعد العبارة PRIMARY KEY إلى إنشاء مفتاح رئيسي مُركّب (Composite Primary Key): ALTER TABLE EMPLOYEES ADD pk_EmployeeID PRIMARY KEY (ID, FName) تغيير عمود يعدّل الاستعلام التالي نوع بيانات العمود ‎StartingDate‎، ويغيّره من النوع ‎date‎ إلى ‎datetime‎، كما يعيّن قيمته الافتراضية عند قيمة التاريخ الحالي. ALTER TABLE Employees ALTER COLUMN StartingDate DATETIME NOT NULL DEFAULT (GETDATE()) حذف القيود Drop Constraint تحذف الشيفرة التالية قيدًا يُسمّى DefaultSalary من جدول الموظفين: ALTER TABLE Employees DROP CONSTRAINT DefaultSalary تنبيه: عليك حذف قيود العمود قبل حذف العمود. الإدراج عبر INSERT إدراج البيانات من جدول آخر باستخدام SELECT يدرج المثال التالي جميع الموظفين (Employees) في جدول العملاء (Customers). ونظرًا لأنّ الجدولين يحتويان على حقول مختلفة، وقد لا تريد نقل جميع الحقول، فستحتاج إلى تحديد الحقول التي ستُدرج القيم فيها، و الحقول التي يجب اختيارها. لست مضطرّا للحفاظ على أسماء الحقول المترابطة (correlating field)، بيْد أنّه ينبغي عدم تغيير نوع البيانات. يفترض هذا المثال أنّ هويّة حقل المعرّف (Id) مُحدّدة، وأنه تلقائيّ التزايد (auto increment). INSERT INTO Customers (FName, LName, PhoneNumber) SELECT FName, LName, PhoneNumber FROM Employees إن كان للجدولين نفس أسماء الحقول، وكنت تريد نقل جميع السجلات من جدول إلى آخر، فيمكنك استخدام الشيفرة التالية: INSERT INTO Table1 SELECT * FROM Table2 إدراج صف جديد تدرج الشيفرة التالية صفًّا جديدًا في الجدول ‎Customers‎. لاحظ أنّنا لم نحدّد قيمة العمود ‎Id‎، ذلك أنّ قيمته ستضاف تلقائيًا. أمّا الأعمدة الأخرى فينبغي أن تُعيّن قيمها. INSERT INTO Customers VALUES ('Zack', 'Smith', 'zack@example.com', '7049989942', 'EMAIL'); إدراج أعمدة معيّنة تدرج الشيفرة التالية صفًّا جديدًا في الجدول ‎Customers‎، لاحظ أنّها لن تُدرج البيانات إلّا في الأعمدة المُحدّدة: INSERT INTO Customers (FName, LName, Email, PreferredContact) VALUES ('Zack', 'Smith', 'zack@example.com', 'EMAIL'); لاحظ أنه لم يتم تقديم أيّ قيمة للعمود ‎PhoneNumber‎. ولاحظ أيضًا أنّه ينبغي تضمين الأعمدة الموسومة بـ ‎not null‎. إدراج عدّة صفوف دفعة واحدة يمكن إدراج عدّة صفوف باستخدام أمر إدراج واحد على النحو التالي: INSERT INTO tbl_name (field1, field2, field3) VALUES (1,2,3), (4,5,6), (7,8,9); قد تحتاج أحيانًا إلى إدراج كمّيات كبيرة من البيانات دُفعة واحدة، عادة ما توفّر أنظمة إدارة قواعد البيانات بعض التوصيات والميزات للمساعدة على ذلك، كما في حالة: MySQL MSSQL الدمج عبر MERGE تسمح العبارة MERGE (تُسمّى أيضًا UPSERT) بإدراج صفوف جديدة أو تحديثها. الهدف من هذه العملية هو إجراء مجموعة كاملة من العمليات تلقائيًا (لضمان بقاء البيانات متسقة)، ومنع حمل الاتصال الزائد (communication overhead) لعبارات SQL في نظام عميل / خادم (client/server system). إجراء عملية الدمج لجعل المصدر يطابق الهدف يجري المثال التالي عملية دمج (MERGE) لجعل الجدول المصدري يطابق الجدول الهدف: MERGE INTO targetTable t USING sourceTable s ON t.PKID = s.PKID WHEN MATCHED AND NOT EXISTS ( SELECT s.ColumnA, s.ColumnB, s.ColumnC INTERSECT SELECT t.ColumnA, t.ColumnB, s.ColumnC ) THEN UPDATE SET t.ColumnA = s.ColumnA ,t.ColumnB = s.ColumnB ,t.ColumnC = s.ColumnC WHEN NOT MATCHED BY TARGET THEN INSERT (PKID, ColumnA, ColumnB, ColumnC) VALUES (s.PKID, s.ColumnA, s.ColumnB, s.ColumnC) WHEN NOT MATCHED BY SOURCE THEN DELETE ; ملاحظة: تمنع العبارة ‎AND NOT EXISTS‎ تحديث السجلات التي لم تتغير. ويتيح استخدام البنية ‎INTERSECT‎ موازنة الأعمدة المعدومة (nullable) بدون مشاكل. MySQL: عدّ أسماء المستخدمين لنفترض أنّنا نريد أن نحسب عدد المستخدمين الذين لهم نفس الاسم. سننشئ أولًا جدولًا ‎users‎ على النحو التالي: create table users( id int primary key auto_increment, name varchar(8), count int, unique key name(name) ); لنفترض الآن أنّنا اكتشفنا وجود مستخدم آخر جديدًا يُدعى Joe، ونودّ أن نأخذه في الحسبان. أولًا، سنتحقّق ممّا إذا كان هناك صفّ يحمل ذلك الاسم، فإن كان الأمر كذلك، حدّثناه وزدنا قيمته بواحد؛ خلاف ذلك، سننشئ صفًّا جديدًا. تستخدم MySQL الصياغة التالية: insert … on duplicate key update …‎‎: insert into users(name, count) values ('Joe', 1) on duplicate key update count=count+1; PostgreSQL: عدّ أسماء المستخدمين لنفترض أنّنا نريد أن نعرف عدد المستخدمين الذين لهم نفس الاسم. سننشئ جدولًا ‎users‎ على النحو التالي: create table users( id serial, name varchar(8) unique, count int ); كما في الفقرة السابقة، سنفترض أنّنا اكتشفنا للتو مستخدمًا جديدًا يُدعى Joe، ونودّ أن نأخذه في الحسبان: تستخدم PostgreSQLالصياغة التالية: insert … on conflict … do update …‎‎: insert into users(name, count) values('Joe', 1) on conflict (name) do update set count = users.count + 1; التطبيق المتقاطع والتطبيق الخارجي cross apply, outer apply تُستخدم العبارة Apply لتطبيق دالة على جدول. سننشئ جدولًا Department لتخزين المعلومات الخاصّة بالأقسام. ثم ننشئ جدولًا Employee يحتوي معلومات حول الموظفين. ينتمي كلّ موظّف إلى قسم معيّن، وبالتالي، فإنّ لجدول الموظّفين مرجعًا متكاملًا (referential integrity) مع جدول الأقسام. يختار الاستعلام الأول البيانات من جدول الأقسام، ثمّ يستخدم العبارة CROSS APPLY لتقييم جدول الموظفين نسبة إلى سجلّات جدول الأقسام. أمّا الاستعلام التالي، [فيضمّ](رابط الفصل 6) جدول الأقسام إلى جدول الموظفين، ثم يعيد جميع السجلات التي تحقّق شرط الضمّ. SELECT * FROM Department D CROSS APPLY ( SELECT * FROM Employee E WHERE E.DepartmentID = D.DepartmentID ) A GO SELECT * FROM Department D INNER JOIN Employee E ON D.DepartmentID = E.DepartmentID لو نظرت إلى النتائج المُعادة، فستلاحظ أنّ الاستعلامين يعيدان نفس النتائج؛ قد تسأل إذن: كيف تختلف CROSS APPLY عن JOIN، وهل أداؤها أحسن. يختار الاستعلام الأول في الشيفرة التالية البيانات من جدول الأقسام، ثمّ يستخدم OUTER APPLY لتقييم جدول الموظفين نسبة إلى كلّ سجل من سجلّات جدول الأقسام. تُعطى قيم الصفوف التي ليس لها مُطابق في جدول الموظفين القيمة المعدومة NULL، كما هو حال الصفّين 5 و 6. يستخدم الاستعلام الثاني الضم الخارجي اليساري LEFT OUTER JOIN بين جدول الأقسام وجدول الموظفين. وكما هو متوقع، يعيد الاستعلام كافّة الصفوف من جدول الأقسام؛ بما فيها الصفوف التي ليس لها مُطابق في جدول الموظفين. SELECT * FROM Department D OUTER APPLY ( SELECT * FROM Employee E WHERE E.DepartmentID = D.DepartmentID ) A GO SELECT * FROM Department D LEFT OUTER JOIN Employee E ON D.DepartmentID = E.DepartmentID GO رغم أنّ الاستعلامين أعلاه يعيدان المعلومات نفسها، فإنّ خطة التنفيذ تختلف بعض الشيء. بيْد أنّه لن يكون هناك اختلاف يُذكر في الأداء. في بعض الحالات، يكون استخدام المعامل APPLY ضروريًا. في المثال التالي، سننشئ دالة جدولية (table-valued function)، تقبل الحقل DepartmentID كمعامل، وتعيد جميع الموظفين المنتمين إلى القسم ذي المعرّف DepartmentID. يختار الاستعلام التالي البيانات من الجدول Department ويستخدم CROSS APPLY مع الدالة التي أنشأناها. يمرَّر الحقل DepartmentID من كل صفّ من الجدول الخارجي - outer table - (أي الجدول Department)، ثمّ يطبّق الدالة على كلّ صف بشكل يماثل الاستعلامات الفرعية المرتبطة (correlated subquery). يستخدم الاستعلام الثاني OUTER APPLY بدلاً من CROSS APPLY، وبالتالي، فعلى عكس التطبيق المتقاطع CROSS APPLY الذي لا يعيد إلّا البيانات المرتبطة (correlated data)، يعيد التطبيق الخارجي OUTER APPLY البيانات غير المرتبطة أيضًا، مع وضع القيم المعدومة (NULLs) في الأعمدة غير الموجودة. CREATE FUNCTION dbo.fn_GetAllEmployeeOfADepartment (@DeptID AS int) RETURNS TABLE AS RETURN ( SELECT * FROM Employee E WHERE E.DepartmentID = @DeptID ) GO SELECT * FROM Department D CROSS APPLY dbo.fn_GetAllEmployeeOfADepartment(D.DepartmentID) GO SELECT * FROM Department D OUTER APPLY dbo.fn_GetAllEmployeeOfADepartment(D.DepartmentID) GO قد يتبادر إلى ذهنك السؤال التالي: هل يمكننا استخدام عمليّة ضمّ بسيطة بدلاً من استخدام الاستعلامات أعلاه؟ الجواب هو "لا"، إذا استبدلت CROSS / OUTER APPLY في الاستعلامات أعلاه بـعمليّات الضمّ INNER JOIN أو LEFT OUTER JOIN، وحدّدت العبارة ON (مثلًا 1 = 1)، ثمّ نفّذت الاستعلام، فسيُطرح الخطأ: The multi-part identifier "D.DepartmentID" could not be bound. ذلك أنّه في عمليّات الضمّ، يكون سياق تنفيذ الاستعلام الخارجي مختلفًا عن سياق تنفيذ الدالة (أو الجدول المشتق - derived table)، ولا يمكنك تمرير قيمة أو متغيّر من الاستعلام الخارجي إلى الدالة كمعامل. لهذا يجب استخدام المعامل APPLY في مثل هذه الاستعلامات. ترجمة -وبتصرّف- للفصول من 23 إلى 28 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: حذف الجداول وقواعد البيانات في SQL المقال سابق: تحديث الجداول في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  22. تستعرض هذه المقالة كيفية تحديث قواعد البيانات، وكيفية إنشاء قواعد بيانات وجداول ودوال جديدة. التحديث عبر UPDATE تُستخدم الكلمة المفتاحية UPDATE لتحديث بيانات الجداول. تحديث جدول من بيانات جدول آخر تملأ الأمثلة الموضحة أدناه الحقل ‎PhoneNumber‎ لأي موظف يكون أيضًا عميلًا ‎Customer‎، وليس له حاليًا رقم هاتف في جدول الموظفين ‎Employees‎. (تستخدم الأمثلة التالية جدولي الموظفين Employees والعملاء Customers) SQL القياسية يحدّث المثال التالي قاعدة البيانات باستخدام استعلام فرعي مربوط (correlated subquery): UPDATE Employees SET PhoneNumber = (SELECT c.PhoneNumber FROM Customers c WHERE c.FName = Employees.FName AND c.LName = Employees.LName) WHERE Employees.PhoneNumber IS NULL SQL:2003 تحديث باستخدام ‎MERGE‎: MERGE INTO Employees e USING Customers c ON e.FName = c.Fname AND e.LName = c.LName AND e.PhoneNumber IS NULL WHEN MATCHED THEN UPDATE SET PhoneNumber = c.PhoneNumber SQL Server تحديث باستخدام ‎INNER JOIN‎: UPDATE Employees SET PhoneNumber = c.PhoneNumber FROM Employees e INNER JOIN Customers c ON e.FName = c.FName AND e.LName = c.LName WHERE PhoneNumber IS NULL تعديل القيم الحالية يستخدم هذا المثال "الجدول Car" من الفصل 1. تتيح لك SQL إمكانية استخدام القيم القديمة في عمليات التحديث. في المثال التالي، تُزاد قيمة ‎TotalCost‎ بمقدار 100 في الصفين ذوي المعرّفين 3 و4: UPDATE Cars SET TotalCost = TotalCost + 100 WHERE Id = 3 or Id = 4 زِيدَت قيمة TotalCost الخاصّة بالسيارة 3 من 100 إلى 200. زيدت قيمة TotalCost الخاصّة بالسيارة 4 من 1254 إلى 1354. يمكن اشتقاق القيمة الجديدة للعمود من قيمته السابقة، أو من قيمة أيّ عمود آخر في الجدول أو من الجدول المضموم (joined table) نفسه. تحديث صفوف معيّنة تعيّن الشيفرة أدناه قيمة حالة الصف ذي المُعرٍّف 4 إلى READY. UPDATE Cars SET Status = 'READY' WHERE Id = 4 تقيّم عبارة ‎WHERE‎ شرطًا منطقيًا في كل صفّ. إذا استوفى الصف ذلك الشرط، فستُحدّث قيمته. خلاف ذلك، يظل الصف دون تغيير. تحديث جميع الصفوف يعيّن المثال التالي العمود "status" الخاص بجميع صفوف الجدول "Cars" إلى القيمة "READY"، التحديث يشمل جميع الصفوف بسبب غياب العبارة ‎WHERE‎ (التي تصفّي الصفوف). UPDATE Cars SET Status = 'READY' التقاط السجلات المُحدّثة قد ترغب في بعض الأحيان في التقاط السجلات التي حُدِّثت للتو. يمكنك ذلك عبر الصياغة التالية: CREATE TABLE #TempUpdated(ID INT) Update TableName SET Col1 = 42 OUTPUT inserted.ID INTO #TempUpdated WHERE Id > 50 إنشاء قاعدة بيانات عبر CREATE يمكن إنشاء قاعدة بيانات باستخدام أمر SQL التالي: CREATE DATABASE myDatabase; سينتج عن الشيفرة أعلاه قاعدة بيانات فارغة باسم myDatabase، والتي يمكن أن تضيف جداول إليها باستخدام العبارة CREATE TABLE. إنشاء جدول عبر CREATE TABLE تُستخدم العبارة CREATE TABLE لإنشاء جدول جديد في قاعدة البيانات. يتألّف تعريف الجدول من قائمة من الأعمدة وأنواعها، إضافة إلى أيّ قيود تكاملية (integrity constraints). table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المعامل الشرح tableName اسم الجدول columns تحتوي راقمة (enumeration) لجميع الأعمدة الموجودة في الجدول إنشاء جدول عبر Select يمكنك إنشاء نسخة مكرّرة من جدول معيّن على النحو التالي: CREATE TABLE ClonedEmployees AS SELECT * FROM Employees; يمكنك استخدام أيّ من الميزات الأخرى لعبارة SELECT لتعديل البيانات قبل نقلها إلى الجدول الجديد. تُنشأ أعمدة الجدول الجديد تلقائيًا انطلاقًا من الصفوف المُختارة. CREATE TABLE ModifiedEmployees AS SELECT Id, CONCAT(FName," ",LName) AS FullName FROM Employees WHERE Id > 10; إنشاء جدول جديد تنشئ الشيفرة التالية جدولًا بسيطًا باسم (‎Employees‎)، يتألّف من معرّف، واسم الموظف الأول، واسم العائلة، إضافة إلى رقم الهاتف: CREATE TABLE Employees( Id int identity(1,1) primary key not null, FName varchar(20) not null, LName varchar(20) not null, PhoneNumber varchar(10) not null ); هذا المثال خاص بالإصدار Transact-SQL، تنشئ العبارة CREATE TABLE جدولًا جديدًا وتضيفه إلى قاعدة البيانات، تُتبع هذه العبارة باسم الجدول (Employees). ثم تعقبه قائمة بأسماء الأعمدة وخصائصها، مثل المعرّف ID. Id int identity(1,1) not null شرح الشيفرة القيمة شرح Id اسم العمود int نوع البيانات identity(1,1) ينصّ على أنّ قيم العمود ستُنشأ تلقائيًا، بداية من 1، ثم تزداد بمقدار 1 في كل صف جديد primary key تنص على أنّ قيم هذا العمود فريدة وغير مكرّرة not null تنص على أنّ قيم العمود لا يمكن أن تكون معدومة إنشاء جدول باستخدام مفتاح خارجي FOREIGN KEY ينشئ المثال أدناه جدولًا للموظفين ‎Employees‎ يحتوي مرجعًا إلى جدول آخر، وهو جدول المدن ‎Cities‎. CREATE TABLE Cities( CityID INT IDENTITY(1,1) NOT NULL, Name VARCHAR(20) NOT NULL, Zip VARCHAR(10) NOT NULL ); CREATE TABLE Employees( EmployeeID INT IDENTITY (1,1) NOT NULL, FirstName VARCHAR(20) NOT NULL, LastName VARCHAR(20) NOT NULL, PhoneNumber VARCHAR(10) NOT NULL, CityID INT FOREIGN KEY REFERENCES Cities(CityID) ); يستعرض الرسم التالي مخططًا توضيحيًا للعلاقة بين قاعدتي البيانات. لاحظ أنّه في السطر الأخير من الشيفرة، يشير العمود ‎CityID‎ من الجدول ‎Employees‎ إلى العمود ‎CityID‎ في الجدول ‎Cities‎: CityID INT FOREIGN KEY REFERENCES Cities(CityID) شرح الشيفرة القمة شرح CityID اسم العمود int نوع العمود FOREIGN KEY إنشاء المفتاح الخارجي (اختياري) REFERENCES Cities(CityID) إنشاء مرجع إلى العمود CityID من جدول المدن تنبيه: لا يجوز إنشاء مرجع إلى جدول غير موجود في قاعدة البيانات. عليك أن تنشئ الجدول ‎Cities‎ أولا، ثم تنشئ الجدول ‎Employees‎ عقب ذلك. إذا فعلت ذلك بترتيب معكوس، فسيُطرَح خطأ. تكرار جدول Duplicate a table يمكنك تكرار جدول معيّن عبر الصياغة التالية: CREATE TABLE newtable LIKE oldtable; INSERT newtable SELECT * FROM oldtable; إنشاء جدول مؤقت يمكن إنشاء جدول مؤقت خاص بالجلسة (session) الحالية. لكنّ الصياغة تختلف بحسب إصدار SQL: PostgreSQL و SQLite CREATE TEMP TABLE MyTable(...); SQL Server CREATE TABLE #TempPhysical(...); يمكن كذلك إنشاء جدول مؤقت مرئي للجميع، وليس حصرًا على الجلسة الحالية على النحو التالي: CREATE TABLE ##TempPhysicalVisibleToEveryone(...); ويمكن أيضًا إنشاء جدول في الذاكرة: DECLARE @TempMemory TABLE(...); إنشاء دالة عبر CREATE FUNCTION تتيح SQL إنشاء دالة جديدة. ينشئ المثال التالي دالة باسم FirstWord، والتي تقبل معاملًا من النوع varchar، وتُعيد قيمة من النوع نفسه. CREATE FUNCTION FirstWord (@input varchar(1000)) RETURNS varchar(1000) AS BEGIN DECLARE @output varchar(1000) SET @output = SUBSTRING(@input, 0, CASE CHARINDEX(' ', @input) WHEN 0 THEN LEN(@input) + 1 ELSE CHARINDEX(' ', @input) END) RETURN @output END شرح الشيفرة الوسيط شرح function_name اسم الدالة list_of_paramenters معاملات الدالة return_data_type نوع القيمة المُعادة function_body متن الدالة scalar_expression القيمة العددية المُعادة من الدالة ترجمة -وبتصرّف- للفصول من 19 إلى 22 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: معالجة الأخطاء والتعديل على قواعد البيانات في SQL المقال السابق: الدمج بين الجداول في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  23. تدمج العبارة JOIN البيانات من جدولين، وتعيد مجموعة مختلطة من الأعمدة من كلا الجدولين، وذلك حسب نوع الضمّ المُستخدم، ومعاييره (كيفية ربط الصفوف من كلا الجدولين). يمكن ضمّ جدولٍ إلى نفسه، أو بأيّ جدول آخر. وإذا كانت هناك حاجة للوصول إلى معلومات من أكثر من جدولين، فيمكن استخدام الضمّ عدّة مرّات في عبارة FROM. الضمّ الذاتي Self Join يمكن ضمّ جدول إلى نفسه، بحيث تتطابق الصفوف مع بعضها البعض وفق شروط معينة. في مثل هذه الحالة، يجب استخدام الكُنى (aliases) للتمييز بين العناصر المكرّرة من الجدول. في المثال التالي، لكلّ موظّف في جدول الموظّفين Employees، يُعاد سجلّ يحتوي الاسم الأول للموظّف، والاسم الأول لمديره. ولمّا كان المدراء هم أيضًا موظفين، فسنضمّ الجدول إلى نفسه: SELECT e.FName AS "Employee", m.FName AS "Manager" FROM Employees e JOIN Employees m ON e.ManagerId = m.Id سيعيد هذا الاستعلام البيانات التالية: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } Employee Manager John James Michael James Johnathon John شرح الاستعلام يحتوي الجدول الأصلي على هذه السجلات: Id FName LName PhoneNumber ManagerId DepartmentId Salary HireDate 1 James Smith 1234567890 NULL 1 1000 01-01-2002 2 John Johnson 2468101214 1 1 400 23-03-2005 3 Michael Williams 1357911131 1 2 600 12-05-2009 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 الخطوة الأولى في تنفيذ الاستعلام هي إجراء جداء ديكارتي لجميع السجلات في الجداول المستخدمة في عبارة FROM. في حالتنا هذه، استخدمنا جدول الموظفين مرّتين، لذا سيبدو الجدول الوسيط كما يلي (أزلنا الحقول غير المستخدمة في المثال): e.Id e.FName e.ManagerId m.Id m.FName m.ManagerId 1 James NULL 1 James NULL 1 James NULL 2 John 1 1 James NULL 3 Michael 1 1 James NULL 4 Johnathon 2 2 John 1 1 James NULL 2 John 1 2 John 1 2 John 1 3 Michael 1 2 John 1 4 Johnathon 2 3 Michael 1 1 James NULL 3 Michael 1 2 John 1 3 Michael 1 3 Michael 1 3 Michael 1 4 Johnathon 2 4 Johnathon 2 1 James NULL 4 Johnathon 2 2 John 1 4 Johnathon 2 3 Michael 1 4 Johnathon 2 4 Johnathon 2 الخطوة التالية هي ترشيح السجلات، والإبقاء على السجلات التي تفي بشرط الضمّ وحسب، أي سجلات الجدول ‎e‎ التي يساوي الحقل ‎ManagerId‎ خاصتها الحقلَ ‎Id‎ في الجدول ‎m‎: e.Id e.FName e.ManagerId m.Id m.FName m.ManagerId 2 John 1 1 James NULL 3 Michael 1 1 James NULL 4 Johnathon 2 2 John 1 بعد ذلك، تُقيّم كل التعبيرات المستخدمة في عبارة SELECT لإعادة الجدول التالي: e.FName m.FName John James Michael James Johnathon John أخيرًا، يُستبدل اسما العمودين ‎e.FName‎ و ‎m.FName‎ بكُنيتيهما: Employee Manager John James Michael James Johnathon John الاختلاف بين الضم الداخلي والخارجي هناك عدّة أنواع من الضمّ في SQL، وتختلف تلك الأنواع عن بعضها من حيث ما إذا كانت الصفوف التي (لا) تحقّق الشرط ستُضمّ أم لا. هذه بعض أهمّ أنواع الضمّ: ‎INNER JOIN‎ و ‎LEFT‎ OUTER‎‎JOINوRIGHT OUTER JOIN‎و‎FULL OUTER ‎JOIN‎(الكلمتان المفتَاحيتان‎INNER‎و‎OUTER‎` اختياريتان). يوضّح الشكل أدناه الاختلافات بين مختلف أنواع الضمّ: تمثل المنطقة الزرقاء النتائج المُعادة من عملية الضمّ، فيما تمثّل المنطقة البيضاء النتائج التي لن تعيدها عملية الضمّ. وهذه صورة لتمثيل الضمّ المتقاطع (Join SQL) مصدر الصورة على سبيل المثال، إليك الجدولين التاليين: A B - - 1 3 2 4 3 5 4 6 لاحظ أنّ القيمتين (1،2) حصريتان للجدول A، أمّا القيمتان (3،4) فمُشتركتان، و القيمتان (5،6) حصريتان لـ B. الضمّ الداخلي يعيد الضم الداخلي تقاطع الجدولين، أي الصفوف المشترك بينهما: select * from a INNER JOIN b on a.a = b.b; select a.*,b.* from a,b where a.a = b.b; a | b --+-- 3 | 3 4 | 4 الضم الخارجي اليساري Left outer join يعيد الضم الخارجي اليساري جميع صفوف A، بالإضافة إلى الصفوف المشتركة مع B: select * from a LEFT OUTER JOIN b on a.a = b.b; a | b --+----- 1 | null 2 | null 3 | 3 4 | 4 الضم الخارجي اليميني Right outer join وبالمثل، يعيد الضمّ الخارجي اليميني كل صفوف B، بالإضافة إلى الصفوف المشتركة في A: select * from a RIGHT OUTER JOIN b on a.a = b.b; a | b -----+---- 3 | 3 4 | 4 null | 5 null | 6 الضمّ الخارجي التام Full outer join يعيد الضمّ الخارجي التام اتحاد A و B، أي جميع الصفوف الموجودة في A وجميع الصفوف الموجودة في B. إذا كانت هناك بيانات في A بدون بيانات مقابلة في B، فسيكون الجزء الخاص بـ B معدوما (null). والعكس صحيح. select * from a FULL OUTER JOIN b on a.a = b.b; a | b -----+----- 1 | null 2 | null 3 | 3 4 | 4 null | 6 null | 5 اصطلاحات الضمّ JOIN Terminology لنفترض أنّ لدينا جدولين A و B، وأنّ بعض صفوفِهما متطابقة (وفق شرط JOIN): هناك عدّة أنواع مختلفة من الضمّ يمكن استخدامها لأجل تضمين أو استبعاد الصفوف التي (لا) تحقق شرط الضمّ في كلا الجانبين. تستخدم الأمثلة أدناه البيانات التالية: CREATE TABLE A ( X varchar(255) PRIMARY KEY ); CREATE TABLE B ( Y varchar(255) PRIMARY KEY ); INSERT INTO A VALUES ('Amy'), ('John'), ('Lisa'), ('Marco'), ('Phil'); INSERT INTO B VALUES ('Lisa'), ('Marco'), ('Phil'), ('Tim'), ('Vincent'); الضمّ الداخلي Inner Join يجمع الضمّ الداخلي بين الصفوف اليسرى واليمنى المتطابقة. SELECT * FROM A JOIN B ON X = Y; X Y ------ ----- Lisa Lisa Marco Marco Phil Phil الضم الخارجي اليساري Left outer join يُسمّى اختصارًا الضمّ اليساري. ويجمع بين الصفوف اليسرى واليمنى التي تحقّق الشرط، مع تضمين الصفوف اليسرى التي لا تحقّق الشرط. SELECT * FROM A LEFT JOIN B ON X = Y; X Y ----- ----- Amy NULL John NULL Lisa Lisa Marco Marco Phil Phil الضم الخارجي اليميني Right outer join يُسمّى اختصارًا الضمّ الأيمن. ويجمع بين الصفوف اليسرى واليمنى التي تحقّق الشرط، مع تضمين الصفوف اليمنى التي لا تحقّق الشرط. SELECT * FROM A RIGHT JOIN B ON X = Y; X Y ----- ------- Lisa Lisa Marco Marco Phil Phil NULL Tim NULL Vincent الضمّ الخارجي التام Full outer join يُسمّى اختصارًا الضمّ التام. وهو اتحاد لعمليتي الضم اليساري واليميني. SELECT * FROM A FULL JOIN B ON X = Y; X Y ----- ------- Amy NULL John NULL Lisa Lisa Marco Marco Phil Phil NULL Tim NULL Vincent الضمّ شبه اليساري يضمّ هذا النوع الصفوفَ اليُسرى التي تتطابق مع الصفوف اليمنى. SELECT * FROM A WHERE X IN (SELECT Y FROM B); X ----- Lisa Marco Phil الضمّ شبه اليميني Right Semi Join يضمّ هذا النوع الصفوف اليمنى التي تطابق الصفوف اليسرى. SELECT * FROM B WHERE Y IN (SELECT X FROM A); Y ----- Lisa Marco Phil لا توجد صياغة للعبارة IN مُخصّصة للضمّ شبه اليساري أو شبه اليميني - كلّ ما عليك فعله هو تبديل مواضع الجدول في SQL. الضمّ شبه اليساري المعكوس Left Anti Semi Join يُضمِّن هذا النوع الصفوفَ اليُسرى التي لا تتطابق مع الصفوف اليمنى. SELECT * FROM A WHERE X NOT IN (SELECT Y FROM B); X ---- Amy John تنبيه: استخدام NOT IN في الأعمدة التي تقبل القيم المعدومة NULL قد يسبّب بعض المشاكل (المزيد من التفاصيل هنا). الضمّ شبه اليميني المعكوس Right Anti Semi Join يُضمِّن هذا النوع الصفوف اليمنى التي لا تطابق الصفوف اليسرى. SELECT * FROM B WHERE Y NOT IN (SELECT X FROM A); Y ------- Tim Vincent لا توجد صياغة للعبارة IN مخصّصة للضمّ شبه اليساري أو شبه اليميني المعكوس - كلّ ما عليك فعله هو تبديل مواضع الجدول في SQL. الضم المتقاطع Cross Join يُجري هذا النوع من الضمّ جداءً ديكارتيًا (Cartesian product) بين الصوف اليسرى والصفوف اليمنى. SELECT * FROM A CROSS JOIN B; X Y ----- ------- Amy Lisa John Lisa Lisa Lisa Marco Lisa Phil Lisa Amy Marco John Marco Lisa Marco Marco Marco Phil Marco Amy Phil John Phil Lisa Phil Marco Phil Phil Phil Amy Tim John Tim Lisa Tim Marco Tim Phil Tim Amy Vincent John Vincent Lisa Vincent Marco Vincent Phil Vincent يكافئ الضمّ المتقاطع ضمًّا داخليًا ذا شرط يتحقّق دائمًا، لذا سيعيد الاستعلام التالي النتيجة نفسها: SELECT * FROM A JOIN B ON 1 = 1; الضمّ الذاتي Self-Join يشير هذا النوع من الضم إلى ضمّ الجدول إلى نفسه. يمكن أن تكون عملية الضمّ الذاتي من أيّ نوع من أنواع الضمّ التي ناقشناها أعلاه. على سبيل المثال، هذا ضمّ ذاتي داخلي ( inner self-join): SELECT * FROM A A1 JOIN A A2 ON LEN(A1.X) < LEN(A2.X); X X ---- ----- Amy John Amy Lisa Amy Marco John Marco Lisa Marco Phil Marco Amy Phil الضمّ الخارجي اليساري Left Outer Join يضمن الضمّ الخارجي اليساري (المعروف أيضًا باسم الضمّ اليساري أو الضمّ الخارجي) تمثيل جميع صفوف الجدول الأيسر؛ وفي حال عدم وجود صفّ مطابق في الجدول الأيمن، فسيُعطى الحقل المقابل القيمةَ ‎NULL‎. سيختار المثال التالي جميع الأقسام (departments) والأسماء الأولى للموظّفين الذين يعملون في تلك الأقسام. وستُعاد الأقسام التي لا تحتوي على أيّ موظفين، مع إعطاء اسم الموظف المقابل لها القيمة NULL: SELECT Departments.Name, Employees.FName FROM Departments LEFT OUTER JOIN Employees ON Departments.Id = Employees.DepartmentId سنحصل على الخرج التالي: Departments.Name Employees.FName HR James HR John HR Johnathon Sales Michael Tech NULL شرح الاستعلام يوجد جدولان في عبارة FROM، وهما: Id FName LName PhoneNumber ManagerId DepartmentId Salary HireDate 1 James Smith 1234567890 NULL 1 1000 01-01-2002 2 John Johnson 2468101214 1 1 400 23-03-2005 3 Michael Williams 1357911131 1 2 600 12-05-2009 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 وهذا هو الجدول الثاني: Id Name 1 HR 2 Sales 3 Tech في المرحلة الأولى، يُنشأ جداء ديكارتي للجدولين، وينتج عنه جدول وسيط. يُغلَّظُ خطّ السجلات التي تفي بشرط الضمّ (والذي هو في هذه الحالة: Departments.Id = Employees.DepartmentId)؛ وتُمرَّر إلى المرحلة التالية من الاستعلام. لمّا كان هذا الضّمّ ضمًّا خارجيًا يساريًا (LEFT OUTER JOIN)، فستُعاد جميع السجلّات الموجودة في الجانب الأيسر من الضمّ (أي الأقسام Departments)، في حين تُعطى السجلات الموجودة على الجانب الأيمن القيمة المعدومة (NULL) في حال لم تُطابق شرط الضمّ. Id Name Id FName LName PhoneNumber ManagerId DepartmentId Salary HireDate 1 HR 1 James Smitd 1234567890 NULL 1 1000 01-01-2002 1 HR 2 John Johnson 2468101214 1 1 400 23-03-2005 1 HR 3 Michael Williams 1357911131 1 2 600 12-05-2009 1 HR 4 Johnatdon Smitd 1212121212 2 1 500 24-07-2016 2 Sales 1 James Smith 1234567890 NULL 1 1000 01-01-2002 2 Sales 2 John Johnson 2468101214 1 1 400 23-03-2005 2 Sales 3 Michael Williams 1357911131 1 2 600 12-05-2009 2 Sales 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 3 Tech 1 James Smith 1234567890 NULL 1 1000 01-01-2002 3 Tech 2 John Johnson 2468101214 1 1 400 23-03-2005 3 Tech 3 Michael Williams 1357911131 1 2 600 12-05-2009 3 Tech 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 بعد ذلك، تُقيّم كل التعبيرات المستخدَمة في عبارة SELECT لإعادة الجدول التالي: Departments.Name Employees.FName HR James HR John Sales Richard Tech NULL الضمّ الضمني Implicit Join يمكن أيضًا إجراء عملية الضمّ على عدّة جداول، حيث توضع في عبارة ‎from‎ مفصولة بالفاصلة ‎,‎، مع تحديد العلاقة بينها في العبارة ‎where‎. تسمى هذه التقنية "الضمّ الضمني" - Implicit Join - (لأنها لا تحتوي فعليًا العبارةَ ‎join‎). تدعم جميع أنظمة معالجة قواعد البيانات (RDBMSs) هذه التقنية، ولكن ينصح بتجنّب استخدامها للأسباب التالية: قد تتداخل صياغة الضمّ الضمني مع صياغة الضمّ المتقاطع (cross join)، وهو ما قد يؤدي إلى إعادة نتائج غير صحيحة، خاصةً إذا كان الاستعلام يحتوي الكثير من عمليات الضمّ. إذا كنت تنوي استخدام الضم المتقاطع، فلن يكون ذلك واضحًا من الصياغة (اكتب CROSS JOIN بدلاً من ذلك)، ومن المحتمل أن يعدّلها شخص ما أثناء صيانة الشيفرة دون أن ينتبه. سيختار المثال التالي أسماء الموظفين الأولى وكذلك أسماء الأقسام التي يعملون فيها: SELECT e.FName, d.Name FROM Employee e, Departments d WHERE e.DeptartmentId = d.Id سنحصل على الخرج التالي: e.FName d.Name James HR John HR Richard Sales الضم المتقاطع CROSS JOIN يُجري الضمّ المتقاطع جداءً ديكارتيًا (Cartesian product) على جدولين (الجداء الديكارتي هو عملية تُجمِّع كلّ صفّ من الجدول الأول مع كل صفّ من الجدول الثاني). على سبيل المثال، إذا كان كلّ من الجدولين ‎TABLEA‎ و ‎TABLEB‎ يحتويان 20 صفًا، فستتألّف النتيجة المُعادة من ‎20*20 = 400‎ صفًّا. إليك المثال التالي: SELECT d.Name, e.FName FROM Departments d CROSS JOIN Employees e; سنحصل على الخرج التالي: d.Name e.FName HR James HR John HR Michael HR Johnathon Sales James Sales John Sales Michael Sales Johnathon Tech James Tech John Tech Michael Tech Johnathon يوصى بكتابة CROSS JOIN بشكل صريح إن أردت إجراء ضمّ ديكارتي دفعًا للُّبس. التطبيق المتقاطع و الضم الحرفي CROSS APPLY & LATERAL JOIN هناك نوع خاص من الضمّ يُسمّى الضمّ الحرفي LATERAL JOIN (أضيف حديثًا إلى الإصدار 9.3 وما بعده من PostgreSQL)، والذي يُعرف أيضًا باسم التطبيق المتقاطع CROSS APPLY أو التطبيق الخارجي OUTER APPLY في كلّ من SQL Server و Oracle. الفكرة الأساسية التي ينبني عليها هذا النوع من الضمّ هي أنه سيتم تطبيق دالة (أو استعلام فرعي مضمّن - inline subquery) على كل الصفوف المضمومة. يتيح هذا التحكم في عملية الضمّ، مثلًا يمكنك الاكتفاء بضمّ أوّل مُدخل يحقّق شرط الضمّ (matching entry) في الجدول الآخر. يكمن الاختلاف بين الضمّ العادي والضمّ الحرفي في حقيقة أنّه يمكنك استخدام عمود سبق أن ضممته في الاستعلام الفرعي (subquery) الذي طبّقته تقاطعيًا (CROSS APPLY). هذه صياغة الضم الحرفي. PostgreSQL 9.3 والإصدارات الأحدث: left | right | inner JOIN LATERAL SQL Server CROSS | OUTER APPLY ‎INNER‎ JOIN‎ LATERAL و ‎CROSS‎ APPLY‎ متكافئتان، وكذلك ‎LEFT JOIN LATERAL‎ و ‎OUTER APPLY‎ إليك المثال التالي (الإصدار 9.3 وما بعده من PostgreSQL): SELECT * FROM T_Contacts --LEFT JOIN T_MAP_Contacts_Ref_OrganisationalUnit ON MAP_CTCOU_CT_UID = T_Contacts.CT_UID AND MAP_CTCOU_SoftDeleteStatus = 1 --WHERE T_MAP_Contacts_Ref_OrganisationalUnit.MAP_CTCOU_UID IS NULL -- 989 LEFT JOIN LATERAL ( SELECT --MAP_CTCOU_UID MAP_CTCOU_CT_UID ,MAP_CTCOU_COU_UID ,MAP_CTCOU_DateFrom ,MAP_CTCOU_DateTo FROM T_MAP_Contacts_Ref_OrganisationalUnit WHERE MAP_CTCOU_SoftDeleteStatus = 1 AND MAP_CTCOU_CT_UID = T_Contacts.CT_UID /* AND ( (__in_DateFrom <= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateTo) AND (__in_DateTo >= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateFrom) ) */ ORDER BY MAP_CTCOU_DateFrom LIMIT 1 ) AS FirstOE وهذا مثال يخصّ SQL-Server: SELECT * FROM T_Contacts --LEFT JOIN T_MAP_Contacts_Ref_OrganisationalUnit ON MAP_CTCOU_CT_UID = T_Contacts.CT_UID AND MAP_CTCOU_SoftDeleteStatus = 1 --WHERE T_MAP_Contacts_Ref_OrganisationalUnit.MAP_CTCOU_UID IS NULL -- 989 -- CROSS APPLY -- = INNER JOIN OUTER APPLY -- = LEFT JOIN ( SELECT TOP 1 --MAP_CTCOU_UID MAP_CTCOU_CT_UID ,MAP_CTCOU_COU_UID ,MAP_CTCOU_DateFrom ,MAP_CTCOU_DateTo FROM T_MAP_Contacts_Ref_OrganisationalUnit WHERE MAP_CTCOU_SoftDeleteStatus = 1 AND MAP_CTCOU_CT_UID = T_Contacts.CT_UID /* AND ( (@in_DateFrom <= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateTo) AND (@in_DateTo >= T_MAP_Contacts_Ref_OrganisationalUnit.MAP_KTKOE_DateFrom) ) */ ORDER BY MAP_CTCOU_DateFrom ) AS FirstOE الضم التام FULL JOIN هناك نوع آخر من الضمّ أقل شهرة من غيره، وهو الضمّ التام FULL JOIN (ملاحظة: لا تدعم MySQL الضمّ التام) يعيد الضمّ التام الخارجي FULL OUTER JOIN جميع صفوف الجدول الأيسر، وكذلك جميع صفوف الجدول الأيمن. ستُدرج صفوف الجدول الأيسر التي ليس لها مُطابِقَات مقابلة في الجدول الأيمن، وكذلك في الحالة المعكوسة. إليك المثال التالي: SELECT * FROM Table1 FULL JOIN Table2 ON 1 = 2 وهذا مثال آخر: SELECT COALESCE(T_Budget.Year, tYear.Year) AS RPT_BudgetInYear ,COALESCE(T_Budget.Value, 0.0) AS RPT_Value FROM T_Budget FULL JOIN tfu_RPT_All_CreateYearInterval(@budget_year_from, @budget_year_to) AS tYear ON tYear.Year = T_Budget.Year إن كنت تستخدم عمليات الحذف اللينة soft-deletes (والتي لا تحذف البيانات بشكل نهائي)، فسيتعيّن عليك التحقق من حالة الحذف الليّن مرة أخرى في عبارة WHERE (لأنّ سلوك الضمّ التام - FULL JOIN - يتصرف بشكل يشبه الاتحاد UNION)؛ عند إجراء الضمّ التام، سيتعيّن عليك عادةً السماح بـاستخدام القيمة المعدومة NULL في عبارة WHERE؛ وفي حال نسيت ذلك، فسيتصرّف الضمّ كما لو كان ضمًّا داخليًا (INNER join)، وهو ما لا تريده عند إجراء الضمّ التام. إليك المثال التالي: SELECT T_AccountPlan.AP_UID ,T_AccountPlan.AP_Code ,T_AccountPlan.AP_Lang_EN ,T_BudgetPositions.BUP_Budget ,T_BudgetPositions.BUP_UID ,T_BudgetPositions.BUP_Jahr FROM T_BudgetPositions FULL JOIN T_AccountPlan ON T_AccountPlan.AP_UID = T_BudgetPositions.BUP_AP_UID AND T_AccountPlan.AP_SoftDeleteStatus = 1 WHERE (1=1) AND (T_BudgetPositions.BUP_SoftDeleteStatus = 1 OR T_BudgetPositions.BUP_SoftDeleteStatus IS NULL) AND (T_AccountPlan.AP_SoftDeleteStatus = 1 OR T_AccountPlan.AP_SoftDeleteStatus IS NULL) الضم العودي Recursive JOIN يُستخدم الضمّ العودي عادة للحصول على بيانات من نوع أب-ابن (parent-child data). في SQL، تُقدّم عمليات الضمّ العودية باستخدام تعبيرات الجدول العادية كما يوضّح المثال التالي: WITH RECURSIVE MyDescendants AS ( SELECT Name FROM People WHERE Name = 'John Doe' UNION ALL SELECT People.Name FROM People JOIN MyDescendants ON People.Name = MyDescendants.Parent ) SELECT * FROM MyDescendants; الضم الداخلي الصريح يستعلم الضمّ الأولي - basic join (يُسمّى أيضًا الضمّ الداخلي - inner join) عن البيانات من جدولين، حيث تُحدَّد العلاقة بينهما في عبارة ‎join‎. يستعلم المثال التالي عن أسماء الموظفين (FName) من جدول الموظفين Employees، وأسماء الأقسام التي يعملون فيها (Name) من جدول الأقسام Departments: SELECT Employees.FName, Departments.Name FROM Employees JOIN Departments ON Employees.DepartmentId = Departments.Id سنحصل على الخرج التالي: Employees.FName Departments.Name James HR John HR Richard Sales الضم في استعلام فرعي Joining on a Subquery غالبًا ما يُستخدم الضمّ في الاستعلامات الفرعية (subquery) للحصول على بيانات مُجمّعة (aggregate data) من جدول يحتوي التفاصيل (الجدول الإبن) وعرضها جنبًا إلى جنب مع السجلات من الجدول الأصلي (الجدول الأب). على سبيل المثال، قد ترغب في الحصول على عدد السجلات الفرعية (child records)، أو متوسط قيم عمود معيّن في السجلات الفرعية، أو الصف ذو القيمة الأكبر أو الأصغر. يستخدم هذا المثال الكُنى (لتسهيل قراءة الاستعلامات التي تشمل عدّة جداول)، يعطي المثال فكرة عامّة عن كيفية صياغة عمليات ضمّ الاستعلامات الفرعية. إذ يعيد جميع صفوف الجدول الأصلي "Purchase Orders"، مع إعادة الصف الأول وحسب لكل سجلّ أصلي (parent record) من الجدول الفرعي PurchaseOrderLineItems. SELECT po.Id, po.PODate, po.VendorName, po.Status, item.ItemNo, item.Description, item.Cost, item.Price FROM PurchaseOrders po LEFT JOIN ( SELECT l.PurchaseOrderId, l.ItemNo, l.Description, l.Cost, l.Price, Min(l.id) as Id FROM PurchaseOrderLineItems l GROUP BY l.PurchaseOrderId, l.ItemNo, l.Description, l.Cost, l.Price ) AS item ON item.PurchaseOrderId = po.Id ترجمة -وبتصرّف- للفصل Chapter 18: JOIN من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: تحديث الجداول في SQL المقال السابق: البحث والتنقيب والترشيح في SQL النسخة العربية الكاملة لكتاب ملاحظات للعاملين بلغة SQL 1.0.0
  24. تستعرض هذه المقالة بعض معاملات SQL المتخصصة في البحث والتنقيب وترشيح النتائج. المعامل LIKE مطابقة الأنماط المفتوحة Match open-ended pattern يطابق حرف البدل ‎%‎ الموضوع في بداية أو نهاية السلسلة النصية (أو كليهما) 0 حرف أو أكثر قبل بداية أو بعد نهاية النمط المراد مطابقته. يسمح استخدام "%' في الوسط بوجود 0 حرف أو أكثر بين جزأي النمط المُراد مُطابقته. سنستخدم جدول الموظفين Employees التالي: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date 1 John Johnson 2468101214 1 1 400 23-03-2005 2 Sophie Amudsen 2479100211 1 1 400 11-01-2010 3 Ronny Smith 2462544026 2 1 600 06-08-2015 4 Jon Sanchez 2454124602 1 1 400 23-03-2005 5 Hilde Knag 2468021911 2 1 800 01-01-2000 تطابق العبارة التالية جميع السجلات التي يحتوي حقل FName خاصتها على السلسلة النصية 'on': SELECT * FROM Employees WHERE FName LIKE '%on%'; سنحصل على الخرج التالي: Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date 3 Ronny Smith 2462544026 2 1 600 06-08-2015 4 Jon Sanchez 2454124602 1 1 400 23-03-2005 يطابق التعبير التالي جميع السجلات التي يبدأ الحقل PhoneNumber خاصتها بالسلسلة النصية "246" في جدول الموظفين. SELECT * FROM Employees WHERE PhoneNumber LIKE '246%'; سنحصل على الخرج التالي: Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date 1 John Johnson 2468101214 1 1 400 23-03-2005 3 Ronny Smith 2462544026 2 1 600 06-08-2015 5 Hilde Knag 2468021911 2 1 800 01-01-2000 تطابق العبارة التالية جميع السجلات التي ينتهي الحقل PhoneNumber خاصتها بالسلسلة النصية "11" في جدول الموظفين. SELECT * FROM Employees WHERE PhoneNumber LIKE '%11' Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date 2 Sophie Amudsen 2479100211 1 1 400 11-01-2010 5 Hilde Knag 2468021911 2 1 800 01-01-2000 يطابق التعبير التالي جميع السجلات التي يساوي الحرف الثالث من حقل Fname خاصتها 'n' في جدول الموظفين. SELECT * FROM Employees WHERE FName LIKE '__n%'; (استخدمنا شرطتين سفليتين قبل 'n' لتخطي أول حرفين) Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date 3 Ronny Smith 2462544026 2 1 600 06-08-2015 4 Jon Sanchez 2454124602 1 1 400 23-03-2005 مطابقة محرف واحد يمكن استخدام أحرف البدل وعلامة النسبة المئوية (%) والشرطة السفلية (_) لتوسيع أنماط الاختيار في SQL. يمكن استخدام المحرف ‎_‎ (الشرطة السفلية) كحرف بدل يمثل حرفًا منفردًا. يبحث النمط التالي عن جميع الموظفين الذين يتألف الحقل Fname خاصتهم من 3 حروف، ويبدأ بالحرف "j" وينتهي بـ "n". SELECT * FROM Employees WHERE FName LIKE 'j_n' يمكن استخدام المحرف ‎_‎ أكثر من مرة بحيث يتصرف كبطاقة بدل (wild card) لمطابقة أنماط معينة. على سبيل المثال، يُطابق النمط أعلاه السلاسل النصية التالية: jon و jan و jen. بيْد أنّه لا يُطابق الأسماء التالية: jn و john و jordan و justin و jason و julian و jillian و joann، لأنّ الشرطة السفلية التي استخدمناها في الاستعلام تتخطى حرفًا واحدًا فقط، لذلك لن تُقبل إلا الحقول المؤلفة من 3 أحرف. يُطابق النمط التالي السلاسل النصية التالية: LaSt و LoSt و HaLt: SELECT * FROM Employees WHERE FName LIKE '_A_T' العبارة ESCAPE في الاستعلام LIKE يمكن إجراء بحث نصي في العبارة ‎LIKE‎ على النحو التالي: SELECT * FROM T_Whatever WHERE SomeField LIKE CONCAT('%', @in_SearchText, '%') ستحدث مشكلة إذا أدخل شخص ما نصُّا من قبيل "50%" أو "a_b" (بصرف النظر عن حقيقة أنه يُفضل البحث عن النص الكامل بدل استخدام ‎LIKE‎). يمكن حل هذه المشكلة باستخدام عبارة ‎LIKE‎: SELECT * FROM T_Whatever WHERE SomeField LIKE CONCAT('%', @in_SearchText, '%') ESCAPE '\' هذا يعني أنّ ‎\‎ ستُعامل كحرف التهريب ESCAPE. أي أنّه يمكنك الآن إضافة ‎\‎ إلى كل حرف في السلسلة النصية التي تبحث عنها، وستكون النتائج صحيحة، حتى لو أدخل المستخدم محارف خاصة مثل ‎%‎ أو ‎_‎. إليك المثال التالي: string stringToSearch = "abc_def 50%"; string newString = ""; foreach(char c in stringToSearch) newString += @"\" + c; sqlCmd.Parameters.Add("@in_SearchText", newString); // sqlCmd.Parameters.Add("@in_SearchText", stringToSearch); بدلا من ملاحظة: وضعنا الخوارزمية أعلاه للتوضيح وحسب، ولن تعمل في حال احتوت السلسلة النصية على رسمة (grapheme) مؤلفة من عدّة أحرف (مثل رموز utf-8). مثلًا، في السلسلة النصية ‎string stringToSearch = "Les Mise\u0301rables";‎، ستحتاج إلى فعل ذلك لكل رسمة، وليس لكل حرف. عليك ألا تستخدم الخوارزمية أعلاه إذا كنت تتعامل مع لغات آسيوية أو شرق آسيوية أو جنوب آسيوية. البحث عن مجموعة من المحارف تطابق العبارة التالية جميع السجلات التي يبدأ حقل FName خاصتها بحرف محصور (أبجديا) بين A و F في جدول الموظفين. SELECT * FROM Employees WHERE FName LIKE '[A-F]%' مطابقة نطاق أو مجموعة يمكن مطابقة حرف واحد داخل نطاق محدد (على سبيل المثال: ‎[a-f]‎) أو مجموعة (على سبيل المثال: ‎[abcdef]‎). يطابق نمط النطاق التالي السلسلة النصية gary، ولكن ليس mary: SELECT * FROM Employees WHERE FName LIKE '[a-g]ary' يُطابق نمط المجموعة التالي mary ولكن ليس gary: SELECT * FROM Employees WHERE Fname LIKE '[lmnop]ary' يمكن أيضًا عكس أو نفي النطاق أو المجموعة بوضع العلامة ‎^‎ قبل النطاق أو المجموعة، فلن يتطابق نمط النطاق التالي مع gary، ولكنه سيتطابق مع mary: SELECT * FROM Employees WHERE FName LIKE '[^a-g]ary' لن يتطابق نمط المجموعة التالي مع mary ولكن سيتطابق مع gary: SELECT * FROM Employees WHERE Fname LIKE '[^lmnop]ary' أحرف البدل Wildcard characters يمكن استخدام أحرف البدل مع المعامل LIKE. تُستخدم أحرف البدل في SQL للبحث عن البيانات داخل جدول معيّن. وهناك أربعة منها، وهي كالتالي: % - بديل عن صفر حرف أو أكثر -- "Lo" اختيار جميع العملاء الذين يقطنون مدينة تبدأ بـ SELECT * FROM Customers WHERE City LIKE 'Lo%'; -- "es" اختيار جميع العملاء الذين يقطنون مدينة تحتوي SELECT * FROM Customers WHERE City LIKE '%es%'; _ - بديل عن حرف واحد -- erlin اختيار جميع العملاء الذين يقطنون مدينة تبدأ بحرف معين، متبوعا بـ SELECT * FROM Customers WHERE City LIKE '_erlin'; [charlist] - مجموعات ونطاقات مؤلفة من الحروف المُراد مُطابقتها -- "a" أو "d" أو "l" اختيار جميع العملاء الذين يقطنون مدينة تبدأ بـ SELECT * FROM Customers WHERE City LIKE '[adl]%'; -- "a" أو "d" أو "l" اختيار جميع العملاء الذين يقطنون مدينة تبدأ بـ SELECT * FROM Customers WHERE City LIKE '[a-c]%'; [‎^ charlist] - تطابق الحروف غير الموجودة داخل القوسين المربعين -- "a" أو "d" أو "l" اختيار جميع العملاء الذين يقطنون مدينة لا تبدأ بـ SELECT * FROM Customers WHERE City LIKE '[^apl]%'; or SELECT * FROM Customers WHERE City NOT LIKE '[apl]%' and city like '_%'; التحقق من الانتماء عبر IN يمكن استخدام العبارة IN للتحقق من إنتماء قيمة إلى مجموعة معينة. تعيد الشيفرة التالية السجلات التي ينتمي مُعرّفها ‎id‎ إلى مجموعة معينة من القيم ((1,8,3)): select * from products where id in (1,8,3) يكافئ الاستعلام أعلاه: select * from products where id = 1 or id = 8 or id = 3 يمكن استخدام IN مع استعلام فرعي على النحو التالي: SELECT * FROM customers WHERE id IN ( SELECT DISTINCT customer_id FROM orders ); ستعيد الشيفرة أعلاه جميع العملاء الذين لديهم طلبات في النظام. ترشيح النتائج باستخدام WHERE و HAVING استخدم BETWEEN لترشيح النتائج تستخدم الأمثلة التالية قاعدتي البيانات Sales و Customers. تذكر أنّ المعامل BETWEEN تضميني (inclusive): استخدام المعامل BETWEEN مع الأعداد يعيد الاستعلام التالي جميع سجلات ‎ItemSales‎ التي ينحصر حقل الكمية quantity خاصتها بين 10 و 17. SELECT * From ItemSales WHERE Quantity BETWEEN 10 AND 17 سنحصل على النتائج التالية: Id SaleDate ItemId Quantity Price 1 2013-07-01 100 10 34.5 4 2013-07-23 100 15 34.5 5 2013-07-24 145 10 34.5 استخدام المعامل BETWEEN مع قيم التاريخ يعيد الاستعلام التالي كافة سجلات ‎ItemSales‎ التي ينحصر حقل ‎SaleDate‎ خاصتها بين التاريخين 11 يوليو 2013 و 24 مايو 2013. SELECT * From ItemSales WHERE SaleDate BETWEEN '2013-07-11' AND '2013-05-24' هذا هو الخرج المتوقع: Id SaleDate ItemId Quantity Price 3 2013-07-11 100 20 34.5 4 2013-07-23 100 15 34.5 5 2013-07-24 145 10 34.5 عند موازنة قيم الوقت (datetime) بدلًا من قيم التاريخ (dates)، قد تحتاج إلى تحويل قيم الوقت إلى قيم التاريخ، أو إضافة أو طرح 24 ساعة للحصول على النتيجة المتوقعة. استخدام المعامل BETWEEN مع القيم النصية يعيد الاستعلام التالي كافة العملاء الذين تنحصر أسماؤهم (أبجديًا) بين الحرفين 'D' و 'L'. في هذه الحالة، سيُعاد العميلان ذوي الرقمين 1 و 3. أما العميل رقم 2 ، الذي يبدأ اسمه بـالحرف "M"، فلن يُعاد: SELECT Id, FName, LName FROM Customers WHERE LName BETWEEN 'D' AND 'L'; هذا مثال حي الخرج: Id FName LName 1 William Jones 3 Richard Davis استخدم HAVING مع الدوال التجميعية على خلاف العبارة ‎WHERE‎ ، يمكن استخدام ‎HAVING‎ مع الدوال التجميعية. الدوال التجميعية (aggregate functions) هي دوال تأخذ القيم الموجودة في في عدة صفوف كمُدخلات (بناء على شروط محددة) وتعيد قيمة معينة. هذه بعض الدوال التجميعية: ‎COUNT()‎ و ‎SUM()‎ و ‎MIN()‎ و ‎MAX()‎. يستخدم هذا المثال الجدول Car من الفصل الأول. SELECT CustomerId, COUNT(Id) AS [Number of Cars] FROM Cars GROUP BY CustomerId HAVING COUNT(Id) > 1 يعيد الاستعلام أعلاه ‎CustomerId‎ وعدد السيارات ‎Number of Cars‎ لأيّ عميل لديه أكثر من سيارة واحدة. في هذا المثال، العميل الوحيد الذي لديه أكثر من سيارة واحدة هو العميل رقم 1. هذا هو الخرج: CustomerId Number of Cars 1 2 استخدام WHERE مع القيم NULL / NOT NULL يعيد المثال التالي جميع سجلات الموظفين (Employee) التي تتساوى قيمة العمود ‎ManagerId‎ خاصتهم مع القيمة المعدومة ‎NULL‎. SELECT * FROM Employees WHERE ManagerId IS NULL النتيجة: Id FName LName PhoneNumber ManagerId DepartmentId 1 James Smith 1234567890 NULL 1 يعيد المثال التالي جميع سجلات الموظفين التي لا تساوي قيمة العمود ‎ManagerId‎ خاصتهم القيمة ‎NULL‎. SELECT * FROM Employees WHERE ManagerId IS NOT NULL ستكون النتيجة كما يلي: Id FName LName PhoneNumber ManagerId DepartmentId 2 John Johnson 2468101214 1 1 3 Michael Williams 1357911131 1 2 4 Johnathon Smith 1212121212 2 1 ملاحظة: لن يعيد الاستعلام أعلاه أيّ نتائج في حال غيّرت صياغة العبارة WHERE إلى ‎WHERE ManagerId = NULL‎ أو ‎WHERE‎ ‎ManagerId‎ <> NULL. معامل التساوي تعيد الشيفرة التالية كل صفوف الجدول ‎Employees‎. SELECT * FROM Employees الخرج: Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date CreatedDate ModifiedDate 1 James Smith 1234567890 NULL 1 1000 01-01-2002 01-01-2002 01-01-2002 2 John Johnson 2468101214 1 1 400 23-03-2005 23-03-2005 01-01-2002 3 Michael Williams 1357911131 1 2 600 12-05-2009 12-05-2009 NULL 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 24-07-2016 01-01-2002 يتيح لك استخدام ‎WHERE‎ في نهاية العبارة ‎SELECT‎ ترشيح الصفوف المُعادة وفق شرط معين. إن أردت مثلًا اشتراط التطابق التام مع قيمة معينة، فاستخدم علامة التساوي ‎=‎: SELECT * FROM Employees WHERE DepartmentId = 1 لن يعيد الاستعلام أعلاه إلا الصفوف التي يساوي الحقل ‎DepartmentId‎ خاصتها القيمة ‎1‎: Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date CreatedDate ModifiedDate 1 James Smith 1234567890 NULL 1 1000 01-01-2002 01-01-2002 01-01-2002 2 John Johnson 2468101214 1 1 400 23-03-2005 23-03-2005 01-01-2002 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 24-07-2016 01-01-2002 لنفترض أنّ متجرًا للألعاب لديه فئة من الألعاب يقل سعرها عن 10 دولارات. يعيد الاستعلام التالي هذه الفئة من الألعاب: SELECT * FROM Items WHERE Price < 10 المعاملان المنطقيان AND و OR يمكنك الجمع بين عدة معاملات معًا لإنشاء شروط ‎WHERE‎ أكثر تعقيدًا. تستخدم الأمثلة التالية الجدول ‎Employees‎ التالي: Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date CreatedDate ModifiedDate 1 James Smith 1234567890 NULL 1 1000 01-01-2002 01-01-2002 01-01-2002 2 John Johnson 2468101214 1 1 400 23-03-2005 23-03-2005 01-01-2002 3 Michael Williams 1357911131 1 2 600 12-05-2009 12-05-2009 NULL 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 24-07-2016 01-01-2002 إليك الاستعلام التالي: SELECT * FROM Employees WHERE DepartmentId = 1 AND ManagerId = 1 سينتج الخرج التالي: Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date CreatedDate ModifiedDate 2 John Johnson 2468101214 1 1 400 23-03-2005 23-03-2005 01-01-2002 وهذا استعلام آخر يستخدم المعامل المنطقي OR: SELECT * FROM Employees WHERE DepartmentId = 2 OR ManagerId = 2 سينتج الخرج التالي: Id FName LName PhoneNumber ManagerId DepartmentId Salary Hire_date CreatedDate ModifiedDate 3 Michael Williams 1357911131 1 2 600 12-05-2009 12-05-2009 NULL 4 Johnathon Smith 1212121212 2 1 500 24-07-2016 24-07-2016 01-01-2002 استخدم IN لإعادة الصفوف التي تنتمي قيمها إلى قائمة معينة يستخدم هذا المثال الجدول "Car". SELECT * FROM Cars WHERE TotalCost IN (100, 200, 300) سيعيد هذا الاستعلام السيارة رقم 2، والتي تبلغ تكلفتها 200، والسيّارة رقم 3، والتي تساوي تكلفتها 100. لاحظ أنّ الاستعلام أعلاه يكافئ استخدام ‎OR‎ عدة مرّات كما هو موضّح في المثال التالي: SELECT * FROM Cars WHERE TotalCost = 100 OR TotalCost = 200 OR TotalCost = 300 استخدم LIKE للبحث عن السلاسل النصية يستخدم المثال التالي الجدول Car: SELECT * FROM Employees WHERE FName LIKE 'John' لن يعيد هذا الاستعلام إلا الموظف رقم 1، والذي يتطابق اسمه الأول مع السلسلة النصية "John". SELECT * FROM Employees WHERE FName like 'John%' يمكنك البحث عن سلسلة نصية فرعية عبر إضافة الرمز ‎%‎: John%‎‎ - تعيد أيّ موظف يبدأ اسمه بـ "John" ، متبوعًا بأي عدد من الأحرف ‎‎%John‎‎ - تعيد أيّ موظف ينتهي اسمه بـ "John" ، يعقبه أي عدد من الأحرف ‎%‎John‎%‎ - تعيد أيّ موظف يتضمّن اسمه السلسلة النصية "John" في المثال أعلاه، سيعيد الاستعلام الموظفَ رقم 2، والذي يحمل الاسم "John"، وكذلك الموظف رقم 4، والذي يحمل الاسم "Johnathon". Where EXISTS في المثال التالي، تختار العبارة WHERE EXISTS سجلّات ‎TableName‎ التي تطابق سجلاتٍ في الجدول ‎TableName1‎. SELECT * FROM TableName t WHERE EXISTS ( SELECT 1 FROM TableName1 t1 where t.Id = t1.Id) استخدام HAVING للتحقق من عدة شروط إليك جدول الطلبات التالي: CustomerId ProductId Quantity Price 1 2 5 100 1 3 2 200 1 4 1 500 2 1 4 50 3 5 6 700 للحصول على العملاء الذين طلبوا المنتَجَين ذوي المُعرّف 2 و 3، يمكن استخدام العبارة HAVING: select customerId from orders where productID in (2,3) group by customerId having count(distinct productID) = 2 الخرج الناتج: customerId 1 لن يختار الاستعلام إلا السجلات ذات معرّفات المنتجات المحددة، والتي تحقق شرط HAVING، أي وجود معرّفين اثنين للمنتجات (productIds)، وليس معرّفًا واحدًا فقط. هذه صياغة أخرى: select customerId from orders group by customerId having sum(case when productID = 2 then 1 else 0 end) > 0 and sum(case when productID = 3 then 1 else 0 end) > 0 لن يختار هذا الاستعلام إلا المجموعات التي لها سجل واحد على الأقل يساوي معرّف منتجه (productID) القيمة 2، وسجل واحد على الأقل يساوي معرّف منتجه 3. ترقيم الصفحات Pagination يمكن وضع حدّ لعدد النتائج المُعادة في استعلام معين، لكنّ الصياغة تختلف بحسب النظام المُستخدم: في إصدار SQL القياسي ISO / ANSI: SELECT * FROM TableName FETCH FIRST 20 ROWS ONLY; MySQL و PostgreSQL و SQLite: SELECT * FROM TableName LIMIT 20; Oracle : SELECT Id, Col1 FROM (SELECT Id, Col1, row_number() over (order by Id) RowNumber FROM TableName) WHERE RowNumber <= 20 SQL Server: SELECT TOP 20 * FROM dbo.[Sale] قد ترغب أحيانًا في تخطّي عدد من نتائج الاستعلام وتأخذ النتائج الموالية لها، يمكنك ذلك عبر الصياغة التالية: ISO / ANSI SQL: SELECT Id, Col1 FROM TableName ORDER BY Id OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLY; MySQL: SELECT * FROM TableName LIMIT 20, 20; -- offset, limit Oracle و SQL Server: SELECT Id, Col1 FROM (SELECT Id, Col1, row_number() over (order by Id) RowNumber FROM TableName) WHERE RowNumber BETWEEN 21 AND 40 PostgreSQL و SQLite: SELECT * FROM TableName LIMIT 20 OFFSET 20; يمكنك كذلك تخطي بعض الصفوف من نتائج الاستعلام على النحو التالي: ISO / ANSI SQL: SELECT Id, Col1 FROM TableName ORDER BY Id OFFSET 20 ROWS MySQL: SELECT * FROM TableName LIMIT 20, 42424242424242; -- تخطي 20 صفا، بالنسبة لعدد الصفوف المأخوذة، استخدم عددا كبيرا يتجاوز عدد الصفوف في الجدول Oracle: SELECT Id, Col1 FROM (SELECT Id, Col1, row_number() over (order by Id) RowNumber FROM TableName) WHERE RowNumber > 20 PostgreSQL: SELECT * FROM TableName OFFSET 20; SQLite: SELECT * FROM TableName LIMIT -1 OFFSET 20; EXCEPT يمكنك استثناء مجموعة من البيانات عبر استخدام الكلمة المفتاحية EXCEPT. إليك المثال التالي: -- ينبغي أن تكون مجموعات البيانات متماثلة SELECT 'Data1' as 'Column' UNION ALL SELECT 'Data2' as 'Column' UNION ALL SELECT 'Data3' as 'Column' UNION ALL SELECT 'Data4' as 'Column' UNION ALL SELECT 'Data5' as 'Column' EXCEPT SELECT 'Data3' as 'Column' -- ==> Data1 و Data2 و Data4 و Data5 EXPLAIN و DESCRIBE استعمال EXPLAIN في استعلامات الاختيار عند وضع ‎Explain‎ قُبالة استعلام ‎select‎، سيعرض محرّك قاعدة البيانات بعض البيانات التي توضّح كيفية تنفيذ الاستعلام. يمكنك استخدام هذه البيانات لفهم الشيفرة ومن ثَمَّ تحسينها، مثلًا، لو لاحظت أنّ الاستعلام لا يستخدم فهرسًا، فيمكنك تحسين استعلامك عن طريق إضافة فهرس. إليك الاستعلام التالي: explain select * from user join data on user.test = data.fk_user; سنحصل على الخرج التالي: id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE user index test test 5 (null) 1 Using where; Using index 1 SIMPLE data ref fk_user fk_user 5 user.tes t 1 (null) يحدد العمود ‎type‎ ما إذا كان الاستعلام يستخدم الفهرس أم لا. وفي العمود ‎possible_keys‎، سترى ما إذا كان بالإمكان تنفيذ الاستعلام بواسطة فهارس أخرى إن لم يتوافر الفهرس. يعرض ‎key‎ الفهرس المستخدم، فيما يعرض العمود ‎key_len‎ حجم عنصر من الفهرس (بالبايتات bytes)، وكلما انخفضت هذه القيمة، زاد عدد عناصر الفهرس التي يمكن تخزينها في مساحة معينة من الذاكرة، وهو ما يسرّع معالجتها. يعرض العمود ‎rows‎ العدد المتوقع للصفوف التي يحتاج الاستعلام إلى جردها، كلما كان هذا العدد أصغر، كان الأداء أفضل. DESCRIBE tablename ‎DESCRIBE‎ و EXPLAIN متماثلتان، بيد أنّ ‎DESCRIBE‎ تعيد معلومات تعريفية لأعمدة الجدول tablename: DESCRIBE tablename; النتيجة: COLUMN_NAME COLUMN_TYPE IS_NULLABLE COLUMN_KEY COLUMN_DEFAULT EXTRA id int(11) NO PRI 0 auto_increment test varchar(255) YES (null) عُرِضت أسماء الأعمدة متبوعة بنوعها، إضافة إلى توضيح ما إذا كان من الممكن استخدام ‎null‎ في العمود، وما إذا كان العمود يستخدم فهرسًا. تُعرض أيضًا القيمة الافتراضية للعمود، وما إذا كان الجدول ينطوي على أّي سلوك خاص، مثل ‎auto_increment‎. العبارة EXISTS إليك جدول العملاء التالي: Id FirstName LastName 1 Ozgur Ozturk 2 Youssef Medi 3 Henry Tai وهذا جدول آخر للطلبيّات: Id CustomerId Amount 1 2 123.50 2 3 14.80 تعيد هذه الشيفرة جميع العملاء الذين قدموا طلبية واحدة على الأقل: SELECT * FROM Customer WHERE EXISTS ( SELECT * FROM Order WHERE Order.CustomerId=Customer.Id ) النتيجة المتوقعة: Id FirstName LastName 2 Youssef Medi 3 Henry Tai يعيد الاستعلام التالي جميع العملاء الذين لم يقدّمو أيّ طلبية: SELECT * FROM Customer WHERE NOT EXISTS ( SELECT * FROM Order WHERE Order.CustomerId = Customer.Id ) النتيجة المتوقّعة: Id FirstName LastName 1 Ozgur Ozturk تُستخدم ‎EXISTS‎ و IN و ‎JOIN‎ أحيانًا للحصول على النتائج نفسها، بيْد أنّ هناك اختلافات في كيفية عملها: تُستخدم ‎EXISTS‎ للتحقق من وجود قيمة في جدول آخر. تُستخدم ‎IN‎ للحصول على قائمة ثابتة. تُستخدم ‎JOIN‎ لاسترجاع البيانات من جداول أخرى. ترجمة -وبتصرّف- للفصول من 11 إلى 17 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: الدمج بين الجداول في SQL المقال السابق: تنفيذ تعليمات شرطية عبر CASE في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
  25. تستعرض هذه المقالة العبارة CASE، والتي تُستخدم لكتابة الشيفرات الشرطية (if-then). استخدام CASE لحساب عدد الصفوف في العمود الذي يلبي شرطا معينًا يمكن استخدام ‎CASE‎ مع SUM لحساب عدد العناصر المطابقة لشرط محدد (تشبه العبارة ‎COUNTIF‎ في Excel.) الحيلة التي سنعتمدها هي أنّنا سنعيد نتائج ثنائية (binary) للدلالة على مطابقة الشرط، حيث يشير 1 إلى أنّ المدخل يطابق الشرط، فيما يشير 0 إلى عدم المطابقة، بعد ذلك سنجمع الوحدات التي حصلنا عليها للحصول على عدد المطابقات. في الجدول ‎ItemSales‎ التالي، سنحاول عدّ العناصر الثمينة (EXPENSIVE): table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } Id ItemId Price PriceRating 1 100 34.5 EXPENSIVE 2 145 2.3 CHEAP 3 100 34.5 EXPENSIVE 4 100 34.5 EXPENSIVE 5 145 10 AFFORDABLE سنستخدم الاستعلام التالي: SELECT COUNT(Id) AS ItemsCount, SUM ( CASE WHEN PriceRating = 'Expensive' THEN 1 ELSE 0 END ) AS ExpensiveItemsCount FROM ItemSales سنحصل على الخرج التالي: ItemsCount ExpensiveItemsCount 5 3 هذا استعلام آخر بديل: SELECT COUNT(Id) as ItemsCount, SUM ( CASE PriceRating WHEN 'Expensive' THEN 1 ELSE 0 END ) AS ExpensiveItemsCount FROM ItemSales البحث الشرطي يمكن استخدام CASE مع العبارة SELECT لتصفية النتائج حسب شرط معيّن، بحيث لا تُعاد إلا النتائج التي تعيد القيمة المنطقية TRUE (هذا يختلف عن استخدام case العادي، والذي يتحقق من التكافؤ مع المُدخل وحسب). SELECT Id, ItemId, Price, CASE WHEN Price < 10 THEN 'CHEAP' WHEN Price < 20 THEN 'AFFORDABLE' ELSE 'EXPENSIVE' END AS PriceRating FROM ItemSales سنحصل على الخرج التالي: Id ItemId Price PriceRating 1 100 34.5 EXPENSIVE 2 145 2.3 CHEAP 3 100 34.5 EXPENSIVE 4 100 34.5 EXPENSIVE 5 145 10 AFFORDABLE الشكل المُختزل لـ CASE يقيّم الشكل المختزل لـ ‎CASE‎ تعبيرًا ما (عادةً ما يكون عمودًا)، ويقارنه بعدة قيم. هذا الشكل أقصر قليلاً من الشكل العادي، ويُعفيك من تكرار التعبير المقيَّم. يمكن استخدام صياغة ‎ELSE‎ في الشكل على النحو التالي: SELECT Id, ItemId, Price, CASE Price WHEN 5 THEN 'CHEAP' WHEN 15 THEN 'AFFORDABLE' ELSE 'EXPENSIVE' END as PriceRating FROM ItemSales من المهم أن تدرك أنه عند استخدام الشكل المختصر، فسيُقيَّم التعبير بالكامل في كل عبارة ‎WHEN‎. لذلك، فإنّ الشيفرة التالية: SELECT CASE ABS(CHECKSUM(NEWID())) % 4 WHEN 0 THEN 'Dr' WHEN 1 THEN 'Master' WHEN 2 THEN 'Mr' WHEN 3 THEN 'Mrs' END قد تعيد القيمة المعدومة ‎NULL‎. لأنّه في كل عبارة ‎WHEN‎، تُستدعى ‎NEWID()‎ مع نتيجة جديدة. هذا يكافئ: SELECT CASE WHEN ABS(CHECKSUM(NEWID())) % 4 = 0 THEN 'Dr' WHEN ABS(CHECKSUM(NEWID())) % 4 = 1 THEN 'Master' WHEN ABS(CHECKSUM(NEWID())) % 4 = 2 THEN 'Mr' WHEN ABS(CHECKSUM(NEWID())) % 4 = 3 THEN 'Mrs' END لذلك يمكن أن تُفوِّت جميع عبارات ‎WHEN‎، لتُنتج القيمة ‎NULL‎. استخدام CASE في عبارة ORDER BY في الشيفرة، أدناه سنستخدم الأرقام 1،2،3 .. لتصنيف الطلب إلى أنواع: SELECT * FROM DEPT ORDER BY CASE DEPARTMENT WHEN 'MARKETING' THEN 1 WHEN 'SALES' THEN 2 WHEN 'RESEARCH' THEN 3 WHEN 'INNOVATION' THEN 4 ELSE 5 END, CITY الخرج الناتج: ID REGION CITY DEPARTMENT EMPLOYEES_NUMBER 12 New England Boston MARKETING 9 15 West San Francisco MARKETING 12 9 Midwest Chicago SALES 8 14 Mid-Atlantic New York SALES 12 5 West Los Angeles RESEARCH 11 10 Mid-Atlantic Philadelphia RESEARCH 13 4 Midwest Chicago INNOVATION 11 2 Midwest Detroit HUMAN RESOURCES 9 استخدام CASE في UPDATE يزيد المثال التالي الأسعار في قاعدة البيانات: UPDATE ItemPrice SET Price = Price * CASE ItemId WHEN 1 THEN 1.05 WHEN 2 THEN 1.10 WHEN 3 THEN 1.15 ELSE 1.00 END استخدام CASE مع القيم المعدومة NULL في هذا المثال، يمثل الرقم "0" االقيم المعروفة، والتي توضوع في البداية، فيما يمثل "1" القيم NULL، و هي موضوعة في آخر الترتيب: SELECT ID ,REGION ,CITY ,DEPARTMENT ,EMPLOYEES_NUMBER FROM DEPT ORDER BY CASE WHEN REGION IS NULL THEN 1 ELSE 0 END, REGION سنحصل على الخرج التالي: ID REGION CITY DEPARTMENT EMPLOYEES_NUMBER 10 Mid-Atlantic Philadelphia RESEARCH 13 14 Mid-Atlantic New York SALES 12 9 Midwest Chicago SALES 8 12 New England Boston MARKETING 9 5 West Los Angeles RESEARCH 11 15 NULL San Francisco MARKETING 12 4 NULL Chicago INNOVATION 11 2 NULL Detroit HUMAN RESOURCES 9 استخدام CASE في عبارة ORDER BY لترتيب السجلات حسب القيمة الدنيا لعمودين لنفترض أنك بحاجة إلى ترتيب السجلات حسب القيمة الدنيا في عمودين. قد تستخدم بعض قواعد البيانات الدالتين غير التجميعيتين ‎MIN()‎ أو ‎LEAST()‎ (مثلا: ‎... ORDER BY MIN(Date1, Date2)‎)، ولكن في SQL القياسية، يجب استخدام التعبير ‎CASE‎. يبحث التعبير ‎CASE‎ في الاستعلام أدناه في العمودين ‎Date1‎ و ‎Date2‎ ، ويبحث عن العمود الذي له أدنى قيمة، ثم يرتب السجلات وفقًا لتلك القيمة. إليك الجدول التالي: Id Date1 Date2 1 2017-01-01 2017-01-31 2 2017-01-31 2017-01-03 3 2017-01-31 2017-01-02 4 2017-01-06 2017-01-31 5 2017-01-31 2017-01-05 6 2017-01-04 2017-01-31 إليك الاستعلام التالي: SELECT Id, Date1, Date2 FROM YourTable ORDER BY CASE WHEN COALESCE(Date1, '1753-01-01') < COALESCE(Date2, '1753-01-01') THEN Date1 ELSE Date2 END الخرج النتائج: Id Date1 Date2 1 2017-01-01 2017-01-31 3 2017-01-31 2017-01-02 2 2017-01-31 2017-01-03 6 2017-01-04 2017-01-31 5 2017-01-31 2017-01-05 4 2017-01-06 2017-01-31 كما ترى ، الصف ذو المعّرف ‎Id = 1‎ جاء أولًا، وذلك لأنّ العمود ‎Date1‎ يحتوي أدنى سجل في الجدول، وهو ‎2017-01-01‎، الصف ذو المعرّف ‎Id‎ = ‎3‎ جاء ثانيًا، لأنّ العمود ‎Date2‎ يحتوي القيمة‎2017-01-02‎‎، وهي ثاني أقل قيمة في الجدول، وهكذا دواليك. لقد رتّبنا السجلات من ‎2017-01-01‎ إلى ‎2017-01-06‎ تصاعديًا، بغض النظر عن العمود ‎Date1‎ أو ‎Date2‎ الذي جاءت منه تلك القيم. ترجمة -وبتصرّف- للفصول 7 و8 و9 من الكتاب SQL Notes for Professionals اقرأ أيضًا: المقال التالي: البحث والتنقيب والترشيح في SQL المقال السابق: التجميع والترتيب في SQL النسخة العربية الكاملة من كتاب ملاحظات للعاملين بلغة SQL 1.0.0
×
×
  • أضف...