سؤال

أنا غير قادر على فهم نقطة Option[T] فئة في سكالا. أعني ، أنا غير قادر على رؤية أي Advanages of None خلال null.

على سبيل المثال ، فكر في الرمز:

object Main{
  class Person(name: String, var age: int){
    def display = println(name+" "+age)
  }

  def getPerson1: Person = {
    // returns a Person instance or null
  }

  def getPerson2: Option[Person] = {
    // returns either Some[Person] or None
  }

  def main(argv: Array[String]): Unit = {
    val p = getPerson1
    if (p!=null) p.display

    getPerson2 match{
      case Some(person) => person.display
      case None => /* Do nothing */
    }
  }
}

لنفترض الآن الطريقة getPerson1 عائدات null, ، ثم المكالمة التي أجريت إلى display في الخط الأول من main لا بد أن تفشل مع NPE. وبالمثل إذا getPerson2 عائدات None, ، ال display سوف تفشل المكالمة مرة أخرى مع بعض الأخطاء المماثلة.

إذا كان الأمر كذلك ، فلماذا تعقد سكالا الأشياء من خلال إدخال غلاف قيمة جديد (Option[T]) بدلاً من اتباع نهج بسيط يستخدم في جافا؟

تحديث:

لقد قمت بتحرير الكود الخاص بي حسب mitchاقتراح. ما زلت غير قادر على رؤية أي ميزة معينة Option[T]. لا بد لي من اختبار الاستثنائي null أو None في كلتا الحالتين. :(

إذا كنت قد فهمت بشكل صحيح من رد مايكل, ، هي الميزة الوحيدة Option[T] هل يخبر المبرمج بشكل صريح هذه الطريقة يمكن أن تعيد لا شيء؟ هل هذا هو السبب الوحيد وراء اختيار التصميم هذا؟

هل كانت مفيدة؟

المحلول

ستحصل على نقطة Option أفضل إذا كنت تجبر نفسك على عدم الاستخدام أبدًا get. هذا بسبب get هو ما يعادل "حسنا ، أرسل لي مرة أخرى إلى الأراضي الفارغة".

لذا ، خذ هذا المثال لك. كيف تتصل display دون استخدام get؟ وهنا بعض البدائل:

getPerson2 foreach (_.display)
for (person <- getPerson2) person.display
getPerson2 match {
  case Some(person) => person.display
  case _ =>
}
getPerson2.getOrElse(Person("Unknown", 0)).display

لن يسمح لك أي من هذه البدائل بالاتصال display على شيء غير موجود.

أما لماذا get موجود ، لا يخبرك سكالا كيف يجب كتابة رمزك. قد يحفزك ذلك بلطف ، ولكن إذا كنت ترغب في العودة إلى أي شبكة أمان ، فهذا هو اختيارك.


قمت بتسميره هنا:

هل الميزة الوحيدة للخيار [t] هي أنها تخبر المبرمج بشكل صريح أن هذه الطريقة لا يمكن أن تعيد أي شيء؟

باستثناء "فقط". لكن اسمحوا لي أن أعيد ذلك بطريقة أخرى: رئيسي ميزة ال Option[T] خلال T هو نوع السلامة. إنه يضمن أنك لن ترسل ملف T طريقة لكائن قد لا يكون موجودًا ، لأن المترجم لن يسمح لك بذلك.

قلت إن عليك اختبار القابلية للإلغاء في كلتا الحالتين ، ولكن إذا نسيت - أو لا تعرف - عليك التحقق من Null ، هل يخبرك المترجم؟ أم أن المستخدمين لديك؟

بالطبع ، نظرًا لقابليتها للتشغيل البيني مع Java ، تسمح Scala بالخلايا الخالية تمامًا كما تفعل Java. لذلك إذا كنت تستخدم مكتبات Java ، إذا كنت تستخدم مكتبات Scala مكتوبة بشكل سيء ، أو إذا كنت تستخدم بشكل سيء مكتوب شخصي مكتبات Scala ، لا يزال يتعين عليك التعامل مع المؤشرات الفارغة.

مزايزان آخران مهمتان Option أستطيع أن أفكر في:

  • الوثائق: سيخبرك توقيع نوع الطريقة ما إذا كان يتم إرجاع كائن دائمًا أم لا.

  • القابلية للتأليف الأحادي.

يستغرق هذا الأخير وقتًا أطول بكثير لتقديره تمامًا ، وهو ليس مناسبًا جيدًا لأمثلة بسيطة ، لأنه يظهر فقط قوتها على التعليمات البرمجية المعقدة. لذا ، سأقدم مثالاً أدناه ، لكنني أدرك جيدًا أنه لا يعني أي شيء باستثناء الأشخاص الذين يحصلون عليه بالفعل.

for {
  person <- getUsers
  email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)

نصائح أخرى

قارن:

val p = getPerson1 // a potentially null Person
val favouriteColour = if (p == null) p.favouriteColour else null

مع:

val p = getPerson2 // an Option[Person]
val favouriteColour = p.map(_.favouriteColour)

الممتلكات الأحادية ربط, ، والتي تظهر في سكالا مثل خريطة الوظيفة ، تسمح لنا بسلسلة عمليات على الكائنات دون القلق بشأن ما إذا كانت "خالية" أم لا.

خذ هذا المثال البسيط قليلاً. لنفترض أننا أردنا العثور على جميع الألوان المفضلة لقائمة الأشخاص.

// list of (potentially null) Persons
for (person <- listOfPeople) yield if (person == null) null else person.favouriteColour

// list of Options[Person]
listOfPeople.map(_.map(_.favouriteColour))
listOfPeople.flatMap(_.map(_.favouriteColour)) // discards all None's

أو ربما نود أن نجد اسم أخت والد والد الشخص:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = getPerson2.flatMap(_.father).flatMap(_.mother).flatMap(_.sister)

آمل أن يلقي هذا بعض الضوء على كيف يمكن للخيارات أن تجعل الحياة أسهل قليلاً.

الفرق دقيق. ضع في اعتبارك أن تكون حقًا وظيفة يجب إرجاع قيمة - لا تعتبر فارغة حقًا "قيمة إرجاع طبيعية" بهذا المعنى ، أكثر أ النوع السفلي/ولا شيء.

ولكن ، بالمعنى العملي ، عندما تسمي وظيفة تُرجع شيئًا اختياريًا ، ستفعل:

getPerson2 match {
   case Some(person) => //handle a person
   case None => //handle nothing 
}

منحت ، يمكنك أن تفعل شيئًا مشابهًا مع NULL - ولكن هذا يجعل دلالات الاتصال getPerson2 واضح بحكم حقيقة أنه يعود Option[Person] (شيء عملي لطيف ، بخلاف الاعتماد على شخص يقرأ المستند والحصول على NPE لأنهم لا يقرؤون المستند).

سأحاول حفر مبرمج وظيفي يمكنه إعطاء إجابة أكثر صرامة مما أستطيع.

بالنسبة لي الخيارات مثيرة للاهتمام حقًا عند التعامل معها من أجل بناء جملة الفهم. مع الأخذ Synesso المثال السابق:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = for {
                                  father <- person.father
                                  mother <- father.mother
                                  sister <- mother.sister
                               } yield sister

إذا كانت أي من المهمة None, ، ال fathersMothersSister سوف يكون None لكن لا NullPointerException سوف تثار. يمكنك بعد ذلك المرور بأمان fathersMothersSisterإلى وظيفة أخذ معلمات الخيار دون القلق. لذلك لا تتحقق من NULL ولا تهتم بالاستثناءات. قارن هذا بإصدار Java المقدم في Synesso مثال.

لديك قدرات تكوين قوية مع الخيار:

def getURL : Option[URL]
def getDefaultURL : Option[URL]


val (host,port) = (getURL orElse getDefaultURL).map( url => (url.getHost,url.getPort) ).getOrElse( throw new IllegalStateException("No URL defined") )

ربما أشار شخص آخر إلى ذلك ، لكنني لم أره:

تتمثل إحدى ميزة مطابقة الأنماط مع الاختيار [t] مقابل الفارق الفارغ إلى أن هذا الخيار هو فئة مختومة ، وبالتالي فإن برنامج التحويل البرمجي Scala سيصدر تحذيرًا إذا أهملت ترميز بعض الحالات أو لا شيء. يوجد علامة برمجية إلى المترجم الذي سيحول التحذيرات إلى أخطاء. لذلك من الممكن منع الفشل في التعامل مع حالة "غير موجودة" في وقت الترجم بدلاً من وقت التشغيل. هذه ميزة هائلة على استخدام القيمة الفارغة.

ليس هناك للمساعدة في تجنب فحص فارغ ، فهو موجود لإجبار فحص فارغ. تصبح النقطة واضحة عندما يكون لدى فصلك 10 حقول ، يمكن أن يكون اثنان منها لاغية. ونظامك لديه 50 فئة أخرى مماثلة. في عالم Java ، تحاول منع NPEs على تلك الحقول التي تستخدم مزيجًا من مزيج من القوى العقلية ، أو اتفاقية التسمية ، أو حتى التعليقات التوضيحية. وكل Java Dev فشل في هذا إلى حد كبير. لا تجعل فئة الخيار قيمًا "لاغية" واضحة بصريًا لأي مطورين يحاولون فهم الرمز ، ولكنه يسمح للمترجم بتطبيق هذا العقد غير المعلن سابقًا.

نسخ من هذا التعليق بواسطة دانييل سبواك ]

إذا كانت الطريقة الوحيدة للاستخدام Option هل كانت تطابق النمط من أجل الحصول على القيم ، ثم نعم ، أوافق على أنه لا يتحسن على الإطلاق أكثر من الفارغ. ومع ذلك ، فأنت تفتقد فئة * ضخمة * من وظائفها. السبب الوحيد المقنع للاستخدام Option إذا كنت تستخدم وظائف الأداة المساعدة ذات الترتيب العالي. على نحو فعال ، تحتاج إلى استخدام طبيعتها الأحادية. على سبيل المثال (على افتراض كمية معينة من تقليم API):

val row: Option[Row] = database fetchRowById 42
val key: Option[String] = row flatMap { _ get “port_key” }
val value: Option[MyType] = key flatMap (myMap get)
val result: MyType = value getOrElse defaultValue

لم يكن هذا أنيق؟ يمكننا في الواقع أن نفعل الكثير إذا استخدمنا for-التفاهات:

val value = for {
row <- database fetchRowById 42
key <- row get "port_key"
value <- myMap get key
} yield value
val result = value getOrElse defaultValue

ستلاحظ أننا لا نتحقق بشكل صريح عن NULL ، لا شيء أو أي من ilk. بيت القصيد من الخيار هو تجنب أي من هذا التحقق. يمكنك فقط سلسلة الحسابات على طول الخط وتتحرك لأسفل حتى تحتاج إلى الحصول على قيمة. في هذه المرحلة ، يمكنك أن تقرر ما إذا كنت تريد إجراء فحص صريح أم لا أبداً يجب أن تفعل) ، وتوفير قيمة افتراضية ، ورمي استثناء ، إلخ.

لم أقم أبدًا بأي مطابقة صريحة ضد Option, ، وأنا أعرف الكثير من مطوري سكالا الآخرين الذين هم في نفس القارب. ذكر لي ديفيد بولاك في اليوم الآخر أنه يستخدم مثل هذا المطابقة الصريحة Option (أو Box, ، في حالة الرفع) كعلامة على أن المطور الذي كتب الكود لا يفهم اللغة تمامًا ومكتبةها القياسية.

لا أقصد أن أكون مطرقة القزم ، لكنك تحتاج حقًا إلى إلقاء نظرة على كيفية استخدام ميزات اللغة في الواقع * في الواقع قبل أن تضربها على أنها عديمة الفائدة. أوافق تمامًا على أن الخيار غير مألوف تمامًا كما استخدمته * ، لكنك لا تستخدمه بالطريقة التي تم تصميمها.

إحدى النقاط التي يبدو أن أي شخص آخر هنا قد أثار أنه على الرغم من أنه يمكنك الحصول على مرجع فارغ ، إلا أن هناك تمييزًا تم تقديمه بواسطة الخيار.

هذا هو يمكنك الحصول عليه Option[Option[A]], ، والتي سوف يسكنها None, Some(None) و Some(Some(a)) أين a هو واحد من السكان المعتاد A. هذا يعني أنه إذا كان لديك نوع من الحاويات ، وتريد أن تكون قادرًا على تخزين المؤشرات الخالية منها ، وإخراجها ، فيجب أن تمرر بعض القيمة المنطقية الإضافية لمعرفة ما إذا كنت قد حصلت بالفعل على قيمة. الثآليل مثل هذا يزخر في واجهات برمجة تطبيقات حاويات Java ولا يمكن حتى توفير بعض المتغيرات الخالية من القفل.

null هو بناء لمرة واحدة ، لا يتكون من نفسه ، إنه متاح فقط لأنواع المرجعية ، ويجبرك على التفكير بطريقة غير متكاملة.

على سبيل المثال ، عند التحقق

if (x == null) ...
else x.foo()

عليك أن تحمل في رأسك في جميع أنحاء else فرع ذلك x != null وهذا قد تم فحصه بالفعل. ومع ذلك ، عند استخدام شيء مثل الخيار

x match {
case None => ...
case Some(y) => y.foo
}

أنت أعرف y ليس كذلك Noneعن طريق البناء - وأنت تعلم أنه لم يكن كذلك null إما ، إذا لم يكن لهار مليار دولار خطأ.

الخيار [t] هو موناد ، وهو مفيد حقًا عند استخدام وظائف عالية الترتيب لمعالجة القيم.

سأقترح عليك قراءة المقالات المدرجة أدناه ، فهي مقالات جيدة حقًا توضح لك سبب كون الخيار [t] مفيدًا وكيف يمكن استخدامه بطريقة وظيفية.

إضافة إلى راندال دعابة إجابة, ، فهم لماذا يتم تمثيل الغياب المحتمل للقيمة Option يتطلب فهم ما Option الأسهم مع العديد من الأنواع الأخرى في Scala - على وجه التحديد ، أنواع النمذجة Monads. إذا كان أحدهم يمثل عدم وجود قيمة مع فارغة ، فإن هذا التمييز في الغياب لا يمكن أن يشارك في العقود التي تشاركها الأنواع الأحادية الأخرى.

إذا كنت لا تعرف ما هي الموناد ، أو إذا لم تلاحظ كيف تم تمثيلها في مكتبة سكالا ، فلن ترى ماذا Option يلعب مع ، ولا يمكنك رؤية ما تفتقده. هناك العديد من الفوائد لاستخدامها Option بدلاً من أن يكون ذلك خاليًا من الجدير بالملاحظة حتى في حالة عدم وجود أي مفهوم موناد (أناقش بعضها في "تكلفة الخيار / بعضها مقابل خالية" مستخدم سكالا موضوع قائمة البريد هنا) ، ولكن الحديث عن ذلك يعتبر العزلة نوعًا ما مثل الحديث عن نوع التكرار الخاص بتطبيق قائمة مرتبطة بخصوصية معينة ، ويتساءل عن سبب ضرورة ذلك ، وكل ذلك أثناء فقدانه في واجهة الحاوية/الحاوية/الخوارزمية الأكثر عمومية. هناك واجهة أوسع في العمل هنا أيضًا ، و Option يوفر نموذج التواجد والإحباط لتلك الواجهة.

أعتقد أن المفتاح موجود في إجابة Synesso: الخيار هو ليس مفيد في المقام الأول باعتباره الاسم المستعار المرهق للخلفية ، ولكن ككائن كامل يمكن أن يساعدك بعد ذلك في منطقك.

المشكلة مع NULL هي أنها قلة من كائن. لا يوجد لديه طرق قد تساعدك على التعامل معها (على الرغم من أن مصمم لغة يمكنك إضافة قوائم طويلة بشكل متزايد من الميزات إلى لغتك التي تحاكي كائنًا إذا شعرت بذلك حقًا).

خيار شيء واحد يمكن أن يفعله ، كما أوضحت ، هو محاكاة Null ؛ عليك بعد ذلك اختبار القيمة غير العادية "لا شيء" بدلاً من القيمة غير العادية "NULL". إذا نسيت ، في كلتا الحالتين ، ستحدث الأشياء السيئة. الخيار يجعل من غير المرجح أن يحدث عن طريق الصدفة ، حيث يتعين عليك كتابة "الحصول" (والتي يجب أن تذكرك بذلك قد يكون NULL ، ER ، لا أقصد شيئًا) ، لكن هذه فائدة صغيرة في مقابل كائن غلاف إضافي.

حيث يبدأ الخيار حقًا في إظهار قوته ، يساعدك على التعامل مع مفهوم i-want-something-ولكنني لا يحدث فعليًا.

دعونا نفكر في بعض الأشياء التي قد ترغب في القيام بها بالأشياء التي قد تكون فارغة.

ربما تريد تعيين قيمة افتراضية إذا كان لديك خالية. لنقارن جافا وسكالا:

String s = (input==null) ? "(undefined)" : input;
val s = input getOrElse "(undefined)"

بدلاً من ذلك مرهقًا إلى حد ما؟: بناء لدينا طريقة تتعامل مع فكرة "استخدام القيمة الافتراضية إذا كنت لاغية". هذا ينظف الكود الخاص بك قليلا.

ربما تريد إنشاء كائن جديد فقط إذا كان لديك قيمة حقيقية. قارن:

File f = (filename==null) ? null : new File(filename);
val f = filename map (new File(_))

Scala أقصر قليلاً وتجنب مصادر الخطأ مرة أخرى. ثم ضع في اعتبارك الفائدة التراكمية عندما تحتاج إلى سلسلة من الأشياء معًا كما هو موضح في الأمثلة من قبل Synesso و Daniel و Paryigmatic.

انها ليست واسع التحسين ، ولكن إذا قمت بإضافة كل شيء ، فإن الأمر يستحق ذلك في كل مكان وفر رمزًا عالي الأداء للغاية (حيث تريد تجنب حتى النفقات العامة الصغيرة لإنشاء بعض الكائنات (x) الغلاف).

استخدام المباراة ليس مفيدًا حقًا من تلقاء نفسه إلا كجهاز لتنبيهك حول حالة NULL/NONE. عندما يكون الأمر مفيدًا حقًا ، يكون عند البدء في تسلقيه ، على سبيل المثال ، إذا كان لديك قائمة بالخيارات:

val a = List(Some("Hi"),None,Some("Bye"));
a match {
  case List(Some(x),_*) => println("We started with " + x)
  case _ => println("Nothing to start with.")
}

الآن يمكنك طي الحالات التي لا شيء والحالات الفارغة القائمة معًا في عبارة واحدة مفيدة تسحب القيمة التي تريدها بالضبط.

لا توجد قيم إرجاع فارغة فقط للتوافق مع Java. يجب ألا تستخدمها خلاف ذلك.

إنه حقًا سؤال أسلوب برمجة. باستخدام Java الوظيفي ، أو عن طريق كتابة أساليب المساعد الخاصة بك ، يمكن أن يكون لديك وظيفة الخيار الخاصة بك ولكن لا تتخلى عن لغة Java:

http://functionaljava.org/examples/#option.bind

لمجرد أن سكالا تتضمنها افتراضيًا لا تجعلها مميزة. تتوفر معظم جوانب اللغات الوظيفية في تلك المكتبة ويمكن أن تتعايش بشكل جيد مع رمز Java الآخر. مثلما يمكنك اختيار برمجة Scala باستخدام Nulls ، يمكنك اختيار برمجة Java بدونها.

الاعتراف مقدمًا بأنه إجابة glib ، الخيار هو موناد.

في الواقع أنا أشاركك الشك معك. حول الخيار ، يزعجني حقًا أن 1) هناك أدنى عام للأداء ، حيث يوجد لور من أغلفة "بعض" التي تم إنشاؤها على الإطلاق. 2) لا بد لي من استخدام الكثير من بعض والخيار في الكود الخاص بي.

لنرى مزايا وعيوب قرار تصميم اللغة هذا يجب أن نأخذ في الاعتبار البدائل. نظرًا لأن Java يتجاهل مشكلة لاغية ، فهي ليست بديلاً. يوفر البديل الفعلي لغة برمجة Fantom. هناك أنواع قابلة للبطولة وغير قابلة للفرق هناك و؟. ؟: المشغلين بدلا من خريطة Scala/flatmap/getorelse. أرى الرصاص التالي في المقارنة:

ميزة الخيار:

  1. لغة أبسط - لا توجد بنيات لغة إضافية مطلوبة
  2. موحد مع أنواع أحادية أخرى

ميزة Nullable:

  1. بناء جملة أقصر في الحالات النموذجية
  2. أداء أفضل (حيث لا تحتاج إلى إنشاء كائنات خيارات جديدة و Lambdas للخريطة ، FlatMap)

لذلك لا يوجد فائز واضح هنا. وملاحظة أخرى. لا توجد ميزة النحوية الرئيسية لاستخدام الخيار. يمكنك تحديد شيء مثل:

def nullableMap[T](value: T, f: T => T) = if (value == null) null else f(value)

أو استخدم بعض التحويلات الضمنية للحصول على بناء جملة pritty مع نقاط.

الميزة الحقيقية لوجود أنواع الخيارات الصريحة هي أنك قادر على ذلك ليس استخدمها في 98 ٪ من جميع الأماكن ، وبالتالي تمنع استثناءات خالية بشكل ثابت. (وفي 2 ٪ الأخرى ، يذكرك نظام النوع بالتحقق بشكل صحيح عند الوصول إليها بالفعل.)

موقف آخر حيث يعمل الخيار ، هو في المواقف التي لا تكون فيها الأنواع قادرة على الحصول على قيمة فارغة. لا يمكن تخزين قيمة NULL في int ، تعويم ، مزدوجة ، إلخ ، ولكن مع خيار يمكنك استخدام لا شيء.

في Java ، ستحتاج إلى استخدام الإصدارات المعبأة (عدد صحيح ، ...) لتلك الأنواع.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top