إذا كنت لا تعرف كيف تختلف
someMap.map{ case (k, v) => k -> foo(v)}
و
someMap.mapValues(foo)
باستثناء بناء الجملة ، أو إذا كنت تشك / لا تعرف ما هي العواقب السيئة التي يمكن أن يؤدي إليها هذا الاختلاف ، وأين identity
، فهذه المقالة مناسبة لك.
خلافًا لذلك ، شارك في الاستطلاع الموجود في نهاية المقالة.
لنبدأ ببساطة
دعونا نحاول بغباء أن نأخذ مثالاً يقع قبل القات ونرى ما سيحدث:
val someMap = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): String = value + "_changed" val resultMap = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}")
من المتوقع طباعة هذا الرمز
resultMap: Map(key1 -> value1_changed, key2 -> value2_changed) resultMapValues: Map(key1 -> value1_changed, key2 -> value2_changed) equality: true
وعلى هذا المستوى تقريبًا ، mapValues
فهم طريقة mapValues
في المراحل المبكرة من تعلم سكالا: حسنًا ، نعم ، هناك مثل هذه الطريقة ، من المناسب تغيير القيم في Map
عندما لا تتغير المفاتيح. وحقاً ، ما الذي يوجد أيضًا للتفكير؟ باسم الطريقة ، كل شيء واضح ، والسلوك واضح.
أمثلة أكثر تعقيدًا
دعنا نعدل مثالنا قليلاً (سأكتب صراحةً أنواعًا حتى لا تعتقد أن هناك نوعًا من العبث مع الضمائر):
case class ValueHolder(value: String) val someMap: Map[String, String] = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): ValueHolder = ValueHolder(value) val resultMap: Map[String, ValueHolder] = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}")
وسوف ينتج هذا الرمز بعد الإطلاق
resultMap: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) resultMapValues: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) equality: true
إنه منطقي وواضح تماما. "يا رجل ، لقد حان الوقت للوصول إلى نهاية المقالة!" - سيقول القارئ. دعنا نجعل إنشاء فصلنا يعتمد على الظروف الخارجية وإضافة اثنين من الفحوصات البسيطة عن البلاهة:
case class ValueHolder(value: String, seed: Int) def foo(value: String): ValueHolder = ValueHolder(value, Random.nextInt()) ... println(s"simple assert for resultMap: ${resultMap.head == resultMap.head}") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}")
عند الإخراج نحصل على:
resultMap: Map(key1 -> ValueHolder(value1,1189482436), key2 -> ValueHolder(value2,-702760039)) resultMapValues: Map(key1 -> ValueHolder(value1,-1354493526), key2 -> ValueHolder(value2,-379389312)) equality: false simple assert for resultMap: true simple assert for resultMapValues: false
من المنطقي أن النتائج ليست متساوية الآن ، بل عشوائية. ولكن انتظر ، لماذا كان التأكيد الثاني false
؟ حقا تغيرت القيم resultMapValues
، لكننا لم نفعل شيئا معهم؟ دعنا نتحقق مما إذا كان كل شيء بالداخل كما هو:
println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues")
وعند الإخراج نحصل على:
resultMapValues: Map(key1 -> ValueHolder(value1,1771067356), key2 -> ValueHolder(value2,2034115276)) resultMapValues: Map(key1 -> ValueHolder(value1,-625731649), key2 -> ValueHolder(value2,-1815306407))

لماذا حدث هذا؟
لماذا تغير println
قيمة Map
؟
حان الوقت للوصول إلى وثائق طريقة mapValues
:
يخبرنا السطر الأول بما كنا نعتقده - فهو يغير Map
، ويطبق الوظيفة التي تم تمريرها في الوسيطات على كل قيمة. ولكن إذا قرأتها بعناية شديدة وحتى النهاية ، اتضح أنه ليست Map
التي يتم إرجاعها ، ولكن "عرض الخريطة" (عرض). وهذه ليست طريقة عرض عادية ( View
) ، والتي يمكنك الحصول عليها باستخدام طريقة view
والتي تحتوي على طريقة force
لحساب صريح. فئة خاصة (الرمز من سكالا الإصدار 2.12.7 ، ولكن بالنسبة لـ 2.11 هناك نفس الشيء تقريبًا):
protected class MappedValues[W](f: V => W) extends AbstractMap[K, W] with DefaultMap[K, W] { override def foreach[U](g: ((K, W)) => U): Unit = for ((k, v) <- self) g((k, f(v))) def iterator = for ((k, v) <- self.iterator) yield (k, f(v)) override def size = self.size override def contains(key: K) = self.contains(key) def get(key: K) = self.get(key).map(f) }
إذا قرأت هذا الرمز ، فسترى أنه لم يتم تخزين أي شيء مؤقتًا ، وفي كل مرة تصل فيها إلى القيم ، سيتم إعادة حسابها. ما نلاحظه في مثالنا.
إذا كنت تعمل بوظائف نقية وكان كل شيء غير قابل للتغيير ، فلن تلاحظ أي فرق. حسنًا ، ربما ينفد الأداء. ولكن ، لسوء الحظ ، ليس كل شيء في عالمنا نظيفًا ومثاليًا ، وباستخدام هذه الأساليب ، يمكنك أن تخطو على أشعل النار (الذي حدث في أحد مشاريعنا ، وإلا لما كانت هذه المقالة).
بالطبع ، لسنا أول من يصادف ذلك. بالفعل في عام 2011 ، تم فتح خطأ كبير في هذه المناسبة (وفي وقت الكتابة ، تم وضع علامة على أنه مفتوح). ويذكر أيضًا طريقة filterKeys
، التي لها نفس المشاكل تمامًا ، لأنها تُطبق على نفس المبدأ.
بالإضافة إلى ذلك ، منذ عام 2015 ، تم تعليق تذكرة لإضافة عمليات تفتيش إلى IntelliJ Idea.
ما يجب القيام به
الحل الأبسط هو عدم استخدام هذه الأساليب بغباء ، لأن بالاسم ، فإن سلوكهم ، في رأيي ، غير واضح للغاية.
خيار أفضل قليلاً هو استدعاء map(identity)
.
identity
، إذا كان أي شخص لا يعرف ، فهذه وظيفة من المكتبة القياسية التي ترجع ببساطة حجة الإدخال الخاصة بها. في هذه الحالة ، يتم تنفيذ العمل الرئيسي من خلال طريقة map
، والتي تنشئ بوضوح Map
عادية. دعنا نتحقق فقط في حالة:
val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo).map(identity) println(s"resultMapValues: $resultMapValues") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}") println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues")
عند الإخراج نحصل
resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) simple assert for resultMapValues: true resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608))
كل شيء على ما يرام :)
إذا كنت لا تزال ترغب في ترك الكسل ، فمن الأفضل تغيير الرمز بحيث يكون واضحًا. يمكنك إنشاء فئة ضمنية باستخدام طريقة mapValues
لـ mapValues
filterKeys
، والتي تعطي اسمًا جديدًا يمكن فهمه لهم. أو استخدم بشكل صريح. .view
والعمل مع مكرر أزواج.
بالإضافة إلى ذلك ، يجدر إضافة التفتيش إلى بيئة / قاعدة التطوير في محلل ثابت / في مكان آخر ، والذي يحذر من استخدام هذه الأساليب. لأنه من الأفضل قضاء بعض الوقت في ذلك الآن بدلاً من السير على أشعل النار واستخلاص العواقب لفترة طويلة لاحقًا.
وإلا كيف يمكنك السير على أشعل النار وكيف داسنا عليها
بالإضافة إلى حالة الاعتماد على الظروف الخارجية ، التي لاحظناها في الأمثلة أعلاه ، هناك خيارات أخرى.
على سبيل المثال ، قيمة قابلة للتغيير (لاحظ هنا أن نظرة سطحية كل شيء "غير قابلة للتغيير"):
val someMap1 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) val someMap2 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) def increment(value: AtomicInteger): Int = value.incrementAndGet() val resultMap: Map[String, Int] = someMap1.map { case (k, v) => k -> increment(v) } val resultMapValues: Map[String, Int] = someMap2.mapValues(increment) println(s"resultMap (1): $resultMap") println(s"resultMapValues (1): $resultMapValues") println(s"resultMap (2): $resultMap") println(s"resultMapValues (2): $resultMapValues")
سيؤدي هذا الرمز إلى هذه النتيجة:
resultMap (1): Map(key1 -> 1, key2 -> 1) resultMapValues (1): Map(key1 -> 1, key2 -> 1) resultMap (2): Map(key1 -> 1, key2 -> 1) resultMapValues (2): Map(key1 -> 2, key2 -> 2)
عندما وصلت إلى someMap2
مرة أخرى ، حصلنا على نتيجة مضحكة.
يمكن أن تضاف المشاكل التي قد تنشأ عند استخدام mapValues
filterKeys
بشكل mapValues
إلى تدهور الأداء ، وزيادة استهلاك الذاكرة و / أو زيادة الحمل على GC ، ولكن هذا يعتمد بشكل أكبر على حالات معينة وقد لا يكون حرجًا للغاية.
يجب عليك أيضًا إضافة طريقة toSeq
إلى بنك أصبع مكابس مماثلة ، والتي ترجع toSeq
كسولًا.
نحن mapValues
على mapValues
الصدفة. تم استخدامه في طريقة ، باستخدام الانعكاس ، أنشأت مجموعة من المعالجات من التكوين: كانت المفاتيح هي معرفات المعالجات ، وكانت القيمة هي إعداداتها ، والتي تم تحويلها بعد ذلك إلى المعالجات نفسها (تم إنشاء مثيل الفئة). نظرًا لأن المعالجات تتكون فقط من وظائف نقية ، فقد عمل كل شيء دون مشاكل ، حتى بشكل ملحوظ لم يؤثر على الأداء (بعد أخذ أشعل النار ، أخذنا القياسات).
ولكن بمجرد أن اضطررت إلى عمل إشارة في أحد المعالجات من أجل معالج واحد فقط لأداء وظيفة ثقيلة ، والتي يتم تخزينها مؤقتًا واستخدامها من قبل معالجات أخرى. ثم بدأت المشاكل في بيئة الاختبار - كود صالح يعمل بشكل جيد محليًا بدأ في التعطل بسبب مشاكل في الإشارة. بالطبع ، كان أول ما فكر في عدم قابلية تشغيل الوظيفة الجديدة هو أن المشاكل مرتبطة بها نفسها. بحثنا مع هذا لفترة طويلة حتى وصلنا إلى استنتاج "بعض الألعاب ، لماذا يتم استخدام حالات مختلفة من المعالجات؟" وكان فقط على تتبع المكدس أنهم وجدوا لنا لعرض mapValues
.
إذا كنت تعمل مع Apache Spark ، فيمكنك أن تتعثر في مشكلة مماثلة عندما تجد فجأة أنه بالنسبة إلى جزء أساسي من التعليمات البرمجية باستخدام .mapValues
يمكنك التقاط
java.io.NotSerializableException: scala.collection.immutable.MapLike$$anon$2
https://stackoverflow.com/questions/32900862/map-can-not-be-serializable-in-scala
https://issues.scala-lang.org/browse/SI-7005
لكن map(identity)
تحل المشكلة ، وعادة لا يوجد دافع / وقت للحفر أعمق.
الخلاصة
يمكن أن تكمن الأخطاء في أكثر الأماكن غير المتوقعة - حتى في تلك الأساليب التي تبدو واضحة بنسبة 100٪. على وجه التحديد ، هذه المشكلة ، في رأيي ، مرتبطة باسم طريقة ضعيف ونوع إرجاع صارم بشكل غير كافٍ.
بالطبع ، من المهم دراسة التوثيق لجميع الأساليب المستخدمة في المكتبة القياسية ، ولكن هذا ليس واضحًا دائمًا ، وبصراحة ، ليس هناك دائمًا ما يكفي من الدافع للقراءة عن "الأشياء الواضحة".
الحوسبة البطيئة وحدها هي مزحة رائعة ، والمقال لا يشجعهم بأي شكل من الأشكال على التخلي عنها. ومع ذلك ، عندما لا يكون الكسل واضحًا - قد يؤدي هذا إلى مشاكل.
في الإنصاف ، ظهرت مشكلة mapValues
بالفعل على حبري في الترجمة ، لكن شخصيًا ، هذا المقال الذي وضعته في رأسي بشكل سيئ جدًا ، لأن كان هناك العديد من الأشياء الأساسية / المعروفة بالفعل ولم يكن من الواضح تمامًا ما يمكن أن يكون خطر استخدام هذه الوظائف:
يلتف أسلوب filterKeys الجدول المصدر دون نسخ أي عناصر. لا يوجد خطأ في ذلك ، لكنك بالكاد تتوقع هذا السلوك من filterKeys
أي أن هناك ملاحظة حول السلوك غير المتوقع ، وأنه في نفس الوقت يمكنك أيضًا أن تخطئ قليلاً ، على ما يبدو ، يعتبر ذلك غير مرجح.
→ جميع التعليمات البرمجية من المقالة في هذا الجوهر