الثوابت المتعددة في TypeScript

من موسوعة حسوب
اذهب إلى: تصفح، ابحث

مقدمة

تسمح لنا الثوابت المتعدّدة بتعريف مجموعة مُسمّاةٍ من الثوابت (set of named constants). استخدام الثوابت المتعدّدة يُسهّل توضيحَ نية استعمال الشيفرة أو إنشاء مجموعة حالات مختلفة. تُوفّر TypeScript كلا من الثوابت المتعددة المعتمِدة على الأعداد وتلك المُعتمِدة على السلاسل النصية كذلك.

الثوابت المتعددة العددية (Numeric enums)

سنبدأ بالثوابت المتعددة العددية، والتي ستكون مألوفة لمن هو آتٍ من لغات البرمجة الأخرى. يُمكن تعريف ثابت متعدّد بالكلمة المفتاحية ‎enum‎:

enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}

في الشيفرة أعلاه، لدينا ثابت متعدّد عدديّ، العنصرُ ‎Up‎ فيه مُهيّأ بالقيمة ‎1‎. جميع العناصر التي تلحقه ستكون قيمها في ازدياد تلقائيّ من هذه النقطة تصاعديًّا. بعبارة أخرى، سيكون للعنصر ‎Direction.Up‎ القيمة ‎1‎، والعنصر ‎Down‎ سيحمل القيمة ‎2‎، أمّا ‎Left‎ فقيمته ‎3‎، وأخيرًا ‎Right‎ قيمته ‎4‎.

يُمكننا كذلك تركُ القيمة الأولى دون تهييء:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

هنا العنصر ‎Up‎ قيمتُه ‎0‎، و‎Down‎ قيمته ‎1‎، وهكذا… الازدياد التلقائي هذا مُفيدٌ في الحالات التي قد لا نهتم فيها بقيم العناصر ذاتها، لكنّنا نهتم بكون كل قيمة مُختلفةً عن باقي القيم في نفس الثابت المتعدّد.

استعمال ثابتٍ متعدّدٍ أمرٌ بسيط، يُمكن فقط الوصول إلى أي عنصر كخاصيّةٍ في الثابت المتعدّدِ نفسه، والتصريح عن الأنواع باستخدام اسم الثابت المتعدّد:

enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)

يُمكن للثوابت المتعددة العددية أن تكون مزيجًا من عناصر ثابتة ومحسوبة (انظر أدناه). يجب على الثوابت المتعددة التي لا تملك عناصرَ مهيّأةً إمّا أن تُهيّأَ أولًا، أو أن تأتي بعد الثوابت المتعددة العددية التي تُهيّأ بثوابت عدديّة أو عناصر ثوابت متعدّدة ثابتةً (constant enum members). بعبارة أخرى، ما يلي لا يجوز:

enum E {
    A = getSomeValue(),

    // خطأ، العنصر
    // 'A'
    // غير مُهيَّئٍ بثابتة، لذا فالعنصر
    // 'B'
    // يحتاج إلى مُهيِّئ
    B,
}

ثوابت السلاسل النّصيّة المتعددة (String enums)

تعمل ثوابت السلاسل النّصيّة المتعددة بطريقةٍ مُشابهة، لكنّها تمتلك بعض الاختلافات الطفيفة أثناء التّنفيذ (runtime) كما هو مُوثَّقٌ أدناه. يجب على جميع العناصر أن تُهيَّأ بثابتة (constant-initialized) باستعمال قيمة سلسلة نصيّة حرفيّة (string literal)، أو باستعمال عنصر ثابت سلسلةٍ نصيّةٍ متعدد (string enum) آخر:

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

ورغم أنّ ثوابت السلاسل النّصيّة المتعددة لا تملك ازديادًا تلقائيّا، إلا أنّ لها منفعةً تتمثّل في سهولة سَلسَلتِها (serialize). بعبارة أخرى، إن توجّب عليك قراءة قيمة ثابت متعدّد عددي أثناء التنفيذ عند التنقيح (debugging)، فستكون القيمة مُبهمةً عادةً، أي أنّها لا تُعطي أي معنًى مُفيدٍ لذاتها (لكن الاقتران العكسي [reverse mapping] قد يُساعد)، تسمح ثوابت السلاسل النّصيّة المتعددة بإعطاء قيمةٍ مُفيدةٍ قابلةٍ للقراءة عند تنفيذ الشيفرة الخاصة بك، وذلك بشكل مستقلٍّ عن اسم عنصر الثابت المتعدّد نفسه.

الثوابت المتعددة المتباينة (Heterogeneous)

يُمكن تقنيًا مزج العناصر العددية والنصية في الثوابت المتعددة، لكنّ القيام بالأمر لا يكون عادةً واضحَ السّبب:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

وهذا غير منصوح به إلّا إن أردت استعمال مزايا زمن التنفيذ في JavaScript بذكاءٍ وبراعة.

العناصر المحسوبة (Computed) والثابتة (Constant)

لكلّ عنصر من عناصر الثوابت المتعددة قيمة ترتبط به، ويُمكن لهذه القيمة أن تكون ثابتةً لا تتغيّر أو محسوبة تُحسَب. يُعدّ عنصر ثابت متعدد ثابتًا إنْ:

  • كان أول عنصر في الثابت المتعدد دون مُهيِّئ (initializer)، وتُعيَّن له القيمة ‎0‎ في هذه الحالة:
// قيمة
// E.X
// ثابتة هنا
enum E { X }
  • لم يملك مُهيِّئًا وكانت قيمة العنصر الذي يسبقُه ثابتةً عددية. في هذه الحالة تكون قيمتُه قيمةَ العنصر الذي يسبقه زائد واحد:
// جميع العناصر في
// 'E1'و 'E2'
// ثابتة
enum E1 { X, Y, Z }

enum E2 {
    A = 1, B, C
}
  • هُيِّئَ بتعبير ثابتٍ متعدّدٍ ثابتٍ (constant enum expression). وهو تعبير TypeScript يُمكن تقديره (evaluated) كاملًا في زمن التّرجمة (compile time). يكون التعبيرُ تعبيرَ ثابتٍ متعدّدٍ ثابتٍ إذا كانَ:
    • تعبيرَ ثابتٍ متعدّدٍ حرفيّ (أي قيمة نصيّة أو عدديّة).
    • مرجعًا يُشير إلى عنصرِ ثابتٍ متعدّدٍ قيمتُه ثابتة مُسبَق التعريف (والذي يُمكن له أن يتأصّل من ثابتٍ متعدّدٍ آخر).
    • تعبيرَ ثابتٍ متعدّدٍ ثابتٍ محصورًا بين قوسين.
    • أحدَ العوامل الأُحاديّة ‎~‎، ‎-‎، ‎+‎المُطبقّة على تعبير ثابتٍ متعدّدٍ ثابتٍ.
    • عوامل ثنائية ‎%‎، ‎/‎، ‎*‎، ‎-‎، ‎+‎، ‎>>>‎، ‎>>‎، ‎<<‎، ‎&‎، ‎|‎، ‎^‎ مع كون تعابير الثوابت المتعددة الثابتة مُعاملاتٍ لها ( operands)، وسيُطلق خطأ أثناء الترجمة (compile time error) عندما تُقدَّرُ التعابير إلى ‎NaN‎ أو ‎Infinity‎.

يُعدّ العنصر محسوبًا في جميع الحالات الأخرى:

enum FileAccess {
    // عناصر ثابتة
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // عنصرٌ محسوب
    G = "123".length
}

ثوابت الاتحاد المُتعدّدة (Union enums) وأنواع عناصر الثوابت المتعدّدة (enum member types)

عناصر الثوابت المُتعدّدة الحرفيّة (literal enum members) تُعدّ مجموعةً فرعيّةً من عناصر الثوابت المتعددة الثابتة. عنصر ثابت متعدّد حرفيّ (literal enum member) هو عنصرُ ثابتٍ متعدّدٍ قيمتُه ثابتة (constant enum member)، لكنّه لا يملك أي قيمة مُهيّأة مسبقًا، أو يملك قيمًا تُهيّأ بما يلي:

  • قيمة سلسلة نصيّة حرفيّة (مثل ‎‎"foo"‎‎، أو ‎‎"bar"‎‎، أو ‎‎"baz"‎‎).
  • قيمة عدد حرفيّة (مثل ‎1‎ أو‎100‎).
  • عاملُ ناقص أُحادي (unary minus) مُطبّقٍ على أي قيمة عدديّة حرفيّة (كالقيمة ‎‎-1‎‎ أو ‎‎-100‎‎).

عندما تكون جميع عناصرِ ثابتٍ متعدّدٍ ذات قيم ثوابت متعدّدة حرفيّة (literal enum values)، فستكون هناك دلاليات خاصّة تسمح لنا باستخدامها بعدّة طرائق.

أولًا، تكون عناصر الثابت المتعدّد أنواعًا كذلك! على سبيل المثال، يُمكن القول بأنّ عناصر مُحدّدةً يُمكن لها أن تملك فقط قيمةَ عنصرِ ثابتٍ مُتعدّد:

enum ShapeKind {
    Circle,
    Square,
}

interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}

let c: Circle = {
    kind: ShapeKind.Square,
    //    ~~~~~~~~~~~~~~~~ خطأ
    radius: 100,
}

ثانيًا، ستُصبحُ أنواع الثوابت المُتعدّدة اتّحادًا (union) لكل عنصرٍ من عناصر الثابت المتعدّد. ورغم أنّنا لم نتطرّق إلى أنواع الاتّحاد (union types) بعد، إلا أنّ كل ما تحتاج إلى معرفته هو أنّ نظام الأنواع (type system) يستطيعُ مع ثوابت الاتّحاد المُتعدّدة (union enums) استغلالَ حقيقة أنّه يعلم مجموعة القيم المضبوطة التي توجد في الثابت المتعدّد نفسه. ولهذا يُمكنُ للغةِ TypeScript اصطياد العِلَل التي قد تحدُث عند مُقارنة القيم على نَحوٍ خاطِئ. مثلًا:

enum E {
    Foo,
    Bar,
}

function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {
        //             ~~~~~~~~~~~
        // خطأ، لا يمكن تطبيقُ العاملِ
        // '!=='
        // على النوعين
        // 'E.Foo' و 'E.Bar'.
    }
}

في المثال أعلاه، نتحقّق أولًا من أنّ ‎x‎ ليسَ هو نفسُه ‎‎E.Foo‎‎. إن نجح ذلك، فلن يُتحقَّقُ ممّا بعد العامل ‎||‎، وستُنفَّذ شيفرة جسم الشرط ‎if‎. لكن إن لم ينجح ذلك، فلا يُمكن للمُتغيّر ‎x‎ أن يكون إلّا ‎E.Foo‎، لذا فمن غير المنطقي التحققّ ممّا إذا كان يُساوي ‎E.Bar‎.

الثوابت المتعددة أثناء التنفيذ (Enums at runtime)

الثوابتُ المتعددةُ كائناتٌ حقيقيّةٌ موجودةٌ أثناء التنفيذ. على سبيل المثال، يُمكن تمرير الثابت المُتعدّد التّالي

enum E {
    X, Y, Z
}

إلى الدوال كما يلي:

function f(obj: { X: number }) {
    return obj.X;
}

// سيعمل هذا الاستدعاء لأنّ
// 'E'
// يملك خاصيّةً عدديّةً اسمُها
// 'X'
f(E);

الاقترانات العكسية (Reverse mappings)

إضافةً إلى إنشاء كائنٍ بأسماء خاصيّات للعناصر، تمتلك عناصر الثوابت المتعددة اقترانًا عكسيًّا كذلك، هذا الاقتران يكون من قيم الثوابت المتعددة إلى أسماء الثوابت المتعددة. مثلًا، في هذا المثال:

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

يُمكن أن تُترجِمه TypeScript إلى ما يُشابه شيفرة JavaScript التالية:

var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

في الشيفرة المُولَّدة هذه، يُترجَم الثابت المتعدد إلى كائن يُخزِّن كلا من الاقترانين الأماميّ (اسم‎ ‎->‎ ‎قيمة‎) والعكسيّ (قيمة ‎ ‎->‎ اسم). المراجع التي تُشير إلى عناصر الثوابت المتعددة الأخرى تكون دائمًا وصولًا إلى الخاصية (property access) ولا تكون سطريّةً (inlined) أبدًا.

تذكّر أن عناصر الثوابت المتعددة النصيّة لا تمتلك اقترانًا عكسيًّا مُولَّدًا.

الثوابت المتعددة المبدوءة بكلمة ‎const

الثوابت المتعدّدة حلٌّ جيدٌ في مُعظم الحالات، لكن أحيانًا تكون المُتطلبات أكثر تقييدًا. لتجنّب دفع ثمن شيفرة مُولَّدةٍ زيادةً عن اللزوم عند الوصول إلى قيم الثوابت المتعدّدة، يُمكن استعمال الكلمة المفتاحيّة ‎ const‎مع الثوابت المتعدّدة. ويُمكن القيام بذلك عبر استعمال الكلمة المفتاحيّة ‎ const‎مع الثابت المتعدّد كما يلي:

const enum Enum {
    A = 1,
    B = A * 2
}

يُمكن لثوابت ‎const‎ المتعدّدة استعمال تعابير الثوابت المتعدّدة الثابتة (constant enum expressions) فقط، وعلى النقيض من الثوابت المتعددة العادية، فإنّها تُحذَفُ بشكلٍ كاملٍ عند الترجمة. وعناصر ثوابت ‎const‎ المتعدّدةُ سطريّةٌ (inlined) في مناطق الاستعمال (use sites). هذا مُمكن لأنه لا يُمكن لثوابت ‎const‎ المتعدّدة امتلاكُ عناصر محسوبة.

const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

الشيفرة المُولَّدة ستكون كالتالي:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

الثوابت المتعددة المُحيطة (Ambient enums)

تُستعمَلُ الثوابت المتعددة المُحيطة لوصف شكل أنواع الثوابت المتعددة (enum types) الموجودة مُسبقًا:

declare enum Enum {
    A = 1,
    B,
    C = 2
}

أحد أهمّ الاختلافات بين الثوابت المتعدّدة المُحيطة وغير المُحيطة هو أن عناصر الثوابت المتعدّدة العادية التي لا تمتلك مهيِّئًا (initializer) تُعَدّ ثابتة إذا كان عنصر الثابت المتعدّد الذي يسبقها ثابتًا. أمّا في الثوابت المتعدّدة المُحيطة (والتي لا تُسبَق بالكلمة المفتاحية ‎const‎) فالعناصر التي لا تمتلك مهيّئًا دائمًا ما تُعَدّ محسوبةً.

مصادر