Go: الاقتراح: المواصفات: إضافة أنواع المجموع / النقابات التمييزية

تم إنشاؤها على ٦ مارس ٢٠١٧  ·  320تعليقات  ·  مصدر: golang/go

هذا اقتراح لأنواع المبالغ ، والمعروف أيضًا باسم النقابات التمييزية. يجب أن تعمل أنواع المجموع في Go بشكل أساسي مثل الواجهات ، باستثناء ما يلي:

  • إنها أنواع قيم ، مثل البُنى
  • الأنواع الواردة فيها ثابتة في وقت الترجمة

يمكن مطابقة أنواع المجموع مع بيان التبديل. يتحقق المترجم من مطابقة جميع المتغيرات. داخل أذرع بيان التبديل ، يمكن استخدام القيمة كما لو كانت من المتغير المطابق.

Go2 LanguageChange NeedsInvestigation Proposal

التعليق الأكثر فائدة

شكرا لخلق هذا الاقتراح. لقد كنت ألعب بهذه الفكرة لمدة عام أو حتى الآن.
ما يلي هو بقدر ما لدي مع اقتراح ملموس. أظن
قد يكون "نوع الاختيار" في الواقع اسمًا أفضل من "نوع المجموع" ، ولكن YMMV.

أنواع المجموع في Go

يتم تمثيل نوع المجموع بنوعين أو أكثر مدمجين مع "|"
المشغل أو العامل.

type: type1 | type2 ...

يمكن أن تحتوي قيم النوع الناتج على نوع واحد فقط من الأنواع المحددة. ال
يتم التعامل مع النوع كنوع واجهة - نوعه الديناميكي هو من نوع
القيمة المخصصة لها.

كحالة خاصة ، يمكن استخدام "لا شيء" للإشارة إلى إمكانية القيمة
يصبح لا شيء.

على سبيل المثال:

type maybeInt nil | int

مجموعة طريقة من نوع الجمع تحمل تقاطع مجموعة الطريقة
بجميع أنواع مكوناته ، باستثناء أي طرق لها نفس الشيء
اسم ولكن توقيعات مختلفة.

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

القيمة الصفرية لنوع المجموع هي القيمة الصفرية للنوع الأول في
المجموع.

عند تعيين قيمة إلى نوع المجموع ، إذا كانت القيمة يمكن أن تتناسب مع المزيد
من أحد الأنواع الممكنة ، ثم يتم اختيار الأول.

على سبيل المثال:

var x int|float64 = 13

قد ينتج عنه قيمة من النوع الديناميكي int ، ولكن

var x int|float64 = 3.13

قد ينتج عنه قيمة بنوع ديناميكي float64.

تطبيق

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

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

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

ال 320 كومينتر

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

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

راجع https://www.reddit.com/r/golang/comments/46bd5h/ama_we_are_the_go_contributors_ask_us_anything/d03t6ji/؟st=ixp2gf04&sh=7d6920db للاطلاع على بعض المناقشات السابقة لتكون على علم.

أعتقد أن هذا تغيير كبير جدًا في نظام النوع لـ Go1 وليس هناك حاجة ملحة.
أقترح إعادة النظر في هذا في السياق الأكبر لـ Go 2.

شكرا لخلق هذا الاقتراح. لقد كنت ألعب بهذه الفكرة لمدة عام أو حتى الآن.
ما يلي هو بقدر ما لدي مع اقتراح ملموس. أظن
قد يكون "نوع الاختيار" في الواقع اسمًا أفضل من "نوع المجموع" ، ولكن YMMV.

أنواع المجموع في Go

يتم تمثيل نوع المجموع بنوعين أو أكثر مدمجين مع "|"
المشغل أو العامل.

type: type1 | type2 ...

يمكن أن تحتوي قيم النوع الناتج على نوع واحد فقط من الأنواع المحددة. ال
يتم التعامل مع النوع كنوع واجهة - نوعه الديناميكي هو من نوع
القيمة المخصصة لها.

كحالة خاصة ، يمكن استخدام "لا شيء" للإشارة إلى إمكانية القيمة
يصبح لا شيء.

على سبيل المثال:

type maybeInt nil | int

مجموعة طريقة من نوع الجمع تحمل تقاطع مجموعة الطريقة
بجميع أنواع مكوناته ، باستثناء أي طرق لها نفس الشيء
اسم ولكن توقيعات مختلفة.

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

القيمة الصفرية لنوع المجموع هي القيمة الصفرية للنوع الأول في
المجموع.

عند تعيين قيمة إلى نوع المجموع ، إذا كانت القيمة يمكن أن تتناسب مع المزيد
من أحد الأنواع الممكنة ، ثم يتم اختيار الأول.

على سبيل المثال:

var x int|float64 = 13

قد ينتج عنه قيمة من النوع الديناميكي int ، ولكن

var x int|float64 = 3.13

قد ينتج عنه قيمة بنوع ديناميكي float64.

تطبيق

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

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

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

rogpeppe كيف case على نوع (أو تأكيد لنوع) ليس عضوًا في المجموع. هل سيكون من الخطأ أيضًا أن يكون لديك مفتاح غير شامل في مثل هذا النوع؟

بالنسبة لمفاتيح الكتابة ، إذا كان لديك

type T int | interface{}

وانت كذلك:

switch t := t.(type) {
  case int:
    // ...

و t يحتوي على واجهة {} تحتوي على عدد صحيح ، هل تتطابق مع الحالة الأولى؟ ماذا لو كانت الحالة الأولى case interface{} ؟

أو يمكن أن تحتوي أنواع المجموع على أنواع خرسانية فقط؟

ماذا عن type T interface{} | nil ؟ إذا كنت تكتب

var t T = nil

ما هو نوع تي؟ أم أن البناء ممنوع؟ يطرح سؤال مشابه مقابل type T []int | nil ، لذلك لا يتعلق الأمر بالواجهات فقط.

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

هذا يعني أنه يمكنك جعل المترجم يخطئ إذا كان لديك:

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

وقمت بتغيير نوع المجموع لإضافة حالة إضافية.

بالنسبة لمفاتيح الكتابة ، إذا كان لديك

اكتب T int | واجهه المستخدم{}

وانت كذلك:

التبديل ر: = ر (نوع) {
حالة int:
// ...
و t يحتوي على واجهة {} تحتوي على عدد صحيح ، هل تتطابق مع الحالة الأولى؟ ماذا لو كانت الحالة الأولى هي واجهة الحالة {}؟

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

يمكن أن تتطابق أنواع المجموع مع أنواع الواجهة ، لكنها لا تزال تكتسب ملمسًا
اكتب للقيمة الديناميكية. على سبيل المثال ، سيكون من الجيد أن يكون لديك:

type R io.Reader | io.ReadCloser

ماذا عن واجهة النوع T {} | لا شيء؟ إذا كنت تكتب

var t T = لا شيء

ما هو نوع تي؟ أم أن البناء ممنوع؟ يطرح سؤال مشابه بخصوص النوع T [] int | لا شيء ، لذلك لا يتعلق الأمر بالواجهات فقط.

وفقًا للاقتراح أعلاه ، تحصل على العنصر الأول
في المجموع الذي يمكن تعيين القيمة إليه ، لذلك
ستحصل على واجهة لا شيء.

في الواقع واجهة {} | لا شيء من الناحية الفنية زائدة عن الحاجة ، لأن أي واجهة {}
يمكن أن يكون لا شيء.

لـ [] int | لا شيء ، nil [] int ليس هو نفسه واجهة صفرية ، لذا فإن
ستكون القيمة الملموسة لـ ([]int|nil)(nil) []int(nil) وليست غير مطبّقة nil .

حالة []int | nil مثيرة للاهتمام. أتوقع أن يعني nil في إعلان النوع دائمًا "قيمة واجهة لا شيء" ، في هذه الحالة

type T []int | nil
var x T = nil

يعني أن x هو واجهة صفرية ، وليس الصفر []int .

ستكون هذه القيمة مختلفة عن القيمة nil []int المشفرة في نفس النوع:

var y T = []int(nil)  // y != x

ألن يكون الصفري مطلوبًا دائمًا حتى لو كان المجموع عبارة عن جميع أنواع القيم؟ وإلا ماذا سيكون var x int64 | float64 ؟ فكرتي الأولى ، بالاستقراء من القواعد الأخرى ، ستكون القيمة الصفرية للنوع الأول ، ولكن ماذا عن var x interface{} | int ؟ كما يشير bcmills ، يجب أن يكون مجموعًا مميزًا لا شيء.

يبدو شديد الدقة.

ستكون مفاتيح النوع الشامل أمرًا رائعًا. يمكنك دائمًا إضافة default: فارغًا عندما لا يكون هذا هو السلوك المطلوب.

يقول الاقتراح "عند تعيين قيمة لنوع المجموع ، إذا كانت القيمة يمكن أن تتناسب مع المزيد
من أحد الأنواع المحتملة ، ثم يتم اختيار النوع الأول ".

حتى مع:

type T []int | nil
var x T = nil

سيكون لـ x نوع ملموس [] int لأن nil يمكن تخصيصه لـ [] int و [] int هو العنصر الأول من النوع. سيكون مساويًا لأية قيمة أخرى [] int (nil).

ألن يكون الصفري مطلوبًا دائمًا حتى لو كان المجموع عبارة عن جميع أنواع القيم؟ وإلا فماذا سيكون var x int64 | float64 يكون؟

يقول الاقتراح "القيمة الصفرية لنوع المجموع هي القيمة الصفرية للنوع الأول في
المجموع. "، لذا فإن الإجابة هي int64 (0).

فكرتي الأولى ، بالاستقراء من القواعد الأخرى ، ستكون القيمة الصفرية للنوع الأول ، ولكن ماذا عن واجهة var x {} | كثافة العمليات؟ كما يشير bcmills ، يجب أن يكون مجموعًا مميزًا لا شيء

لا ، ستكون القيمة المعتادة للواجهة صفر في هذه الحالة. هذا النوع (الواجهة {} | لا شيء) مكرر. ربما يكون من الجيد جعله مترجمًا لتحديد أنواع المجموع حيث يكون أحد العناصر مجموعة شاملة لعنصر آخر ، حيث لا يمكنني حاليًا رؤية أي نقطة في تعريف مثل هذا النوع.

القيمة الصفرية لنوع المجموع هي القيمة الصفرية للنوع الأول في المجموع.

هذا اقتراح مثير للاهتمام ، ولكن نظرًا لأن نوع المجموع يجب أن يسجل في مكان ما نوع القيمة التي يحملها حاليًا ، أعتقد أنه يعني أن القيمة الصفرية لنوع المجموع ليست كلها بايت - صفر ، مما يجعلها مختلفة عن كل نوع آخر في Go. أو ربما يمكننا إضافة استثناء مفاده أنه في حالة عدم وجود معلومات النوع ، فإن القيمة هي القيمة الصفرية من النوع الأول المدرج ، ولكن بعد ذلك لست متأكدًا من كيفية تمثيل nil إذا لم تكن كذلك النوع الأول مدرج.

لذا فإن (stuff) | nil يكون منطقيًا فقط عندما لا يكون أي شيء في (الأشياء) صفرًا و nil | (stuff) يعني شيئًا مختلفًا اعتمادًا على ما إذا كان أي شيء في الأشياء يمكن أن يكون معدومًا؟ ما هي القيمة التي يضيفها لا شيء؟

ianlancetaylor أعتقد أن العديد من اللغات الوظيفية تطبق أنواع مجموع (مغلقة) بشكل أساسي كما تفعل في لغة سي

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

إذا كانت مؤشرات which في حقول الاتحاد بالترتيب ، 0 = a ، 1 = b ، 2 = c ، فإن تعريف القيمة الصفرية يعمل على جميع البايتات هي صفر. وستحتاج إلى تخزين الأنواع في مكان آخر ، على عكس الواجهات. ستحتاج أيضًا إلى معالجة خاصة لعلامة nil من نوع ما أينما تخزن معلومات الكتابة.

هذا من شأنه أن يجعل أنواع قيم الاتحاد بدلاً من الواجهات الخاصة ، وهو أمر مثير للاهتمام أيضًا.

هل هناك طريقة لجعل كل القيمة الصفرية تعمل إذا كان الحقل الذي يسجل النوع يحتوي على قيمة صفرية تمثل النوع الأول؟ أفترض أن إحدى الطرق الممكنة لتمثيل ذلك ستكون:

type A = B|C
struct A {
  choice byte // value 0 or 1
  value ?// (thing big enough to store B | C)
}

[تعديل]

آسف jimmyfrasche ضربني لكمة.

هل هناك أي شيء مضاف بواسطة لا شيء لا يمكن فعله

type S int | string | struct{}
var None struct{}

؟

يبدو أنه يتجنب الكثير من الالتباس (الذي لدي ، على الأقل)

أو أفضل

type (
     None struct{}
     S int | string | None
)

بهذه الطريقة يمكنك كتابة التبديل على None والتعيين بـ None{}

jimmyfrasche struct{} لا يساوي nil . إنها تفاصيل بسيطة ، لكنها ستجعل مفاتيح الكتابة على المبالغ تتباعد دون داع (؟) عن مفاتيح النوع في الأنواع الأخرى.

bcmills لم يكن في

rogpeppe ماذا هذه الطباعة؟

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

سأفترض "Reader"

jimmyfrasche أفترض ReadCloser ، تمامًا كما ستحصل عليه من نوع التبديل على أي واجهة أخرى.

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

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

منح:

 var x int | nil = nil

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

الاحتمال الآخر هو السماح بنوع لا شيء إلا إذا كان العنصر الأول ، ولكن
يستبعد إنشاءات مثل:

var t nil | int
var u float64 | t

jimmyfrasche أفترض أن

نعم فعلا.

bcmills إنه https://play.golang.org/p/PzmWCYex6R

أنا لا أفهم هذا. لماذا "هذا [...] يجب أن يكون صالحًا لمبدل النوع لطباعة ReadCloser"
مثل أي نوع واجهة ، لن يخزن نوع المجموع أكثر من القيمة الملموسة لما يحتويه.

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

أي عندما تقوم بتعيين شيء ما إلى نوع (I1 | I2) حيث يكون كل من I1 و I2 من أنواع الواجهة ، فلا يمكن معرفة ما إذا كانت القيمة التي أدخلتها معروفة بتنفيذ I1 أو I2 في ذلك الوقت.

إذا كان لديك نوع io.ReadCloser | io.Reader ، لا يمكنك التأكد عند كتابة مفتاح التبديل أو التأكيد على io.Reader أنه ليس io.ReadCloser ما لم يتم التعيين إلى نوع مجموع unboxes وإعادة صناديق الواجهة.

الذهاب في الاتجاه الآخر ، إذا كان لديك io.Reader | io.ReadCloser إما أنه لن يقبل أبدًا io.ReadCloser لأنه ينتقل تمامًا من اليمين إلى اليسار أو سيتعين على التطبيق البحث عن واجهة "أفضل مطابقة" من جميع الواجهات في المجموع ولكن لا يمكن تحديد ذلك جيدًا.

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

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

griesemer نعم ، هذا صحيح.

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

تضمين التغريدة

إذا كان لديك نوع io.ReadCloser | io.Reader ، لا يمكنك التأكد عند كتابة مفتاح التبديل أو التأكيد على io.Reader أنه ليس io.ReadCloser ما لم يتم التعيين إلى نوع مجموع unboxes وإعادة صناديق الواجهة.

إذا كان لديك هذا النوع ، فأنت تعلم أنه دائمًا ما يكون io.Reader (أو لا شيء ، لأن أي قارئ io يمكن أن يكون صفريًا أيضًا). البديلان ليسا حصريين - نوع المجموع كما هو مقترح هو "شامل" أو "ليس حصريًا أو".

الذهاب في الاتجاه الآخر ، إذا كان لديك io.Reader | io.ReadCloser إما أنه لن يقبل أبدًا io.ReadCloser لأنه ينتقل تمامًا من اليمين إلى اليسار أو سيتعين على التطبيق البحث عن واجهة "أفضل مطابقة" من جميع الواجهات في المجموع ولكن لا يمكن تحديد ذلك جيدًا.

إذا كنت تقصد بعبارة "الذهاب في الاتجاه الآخر" التخصيص لهذا النوع ، فإن الاقتراح يقول:

"عند تعيين قيمة إلى نوع المجموع ، إذا كانت القيمة تتناسب مع المزيد
من أحد الأنواع المحتملة ، ثم يتم اختيار النوع الأول ".

في هذه الحالة ، يمكن أن يتناسب io.ReadCloser مع كل من io.Reader و io.ReadCloser ، لذلك يختار io.Reader ، ولكن لا توجد طريقة في الواقع لمعرفة ما بعد ذلك. لا يوجد فرق يمكن اكتشافه بين النوع io.Reader والنوع io.Reader | io.ReadCloser ، لأن io.Reader يمكنه أيضًا الاحتفاظ بجميع أنواع الواجهات التي تنفذ io.Reader. لهذا السبب أظن أنه قد يكون فكرة جيدة جعل المترجم يرفض أنواعًا كهذه. على سبيل المثال ، يمكن أن يرفض أي نوع مجموع يتضمن الواجهة {} لأن الواجهة {} يمكن أن تحتوي بالفعل على أي نوع ، لذلك لا تضيف المؤهلات الإضافية أي معلومات.

rogpeppe ، هناك الكثير من الأشياء التي

ما يقلقني هو تعيين قيمة محاصرة بالفعل في واجهة لمتغير مجموع مكتوب.

دعنا ، في الوقت الحالي ، نستخدم المثال السابق ونقول إن RC عبارة عن بنية يمكن تخصيصها لـ io.ReadCloser.

إذا قمت بذلك

var v io.ReadCloser | io.Reader = RC{}

النتائج واضحة وواضحة.

ومع ذلك ، إذا قمت بذلك

var r io.Reader = RC{}
var v io.ReadCloser | io.Reader = r

الشيء الوحيد المعقول الذي يجب فعله هو أن يكون v store r كقارئ io.io. لكن هذا يعني أنه عندما تكتب مفتاح التشغيل v ، فلن تكون متأكدًا من أنه عندما تضغط على io. اقرأ عن قرب. ستحتاج إلى شيء مثل هذا:

switch v := v.(type) {
case io.ReadCloser: useReadCloser(v)
case io.Reader:
  if rc, ok := v.(io.ReadCloser); ok {
    useReadCloser(rc)
  } else {
    useReader(v)
  }
}

الآن ، هناك معنى io.ReadCloser <: io.Reader ، ويمكنك فقط عدم السماح بذلك ، كما اقترحت ، لكنني أعتقد أن المشكلة أكثر جوهرية وقد تنطبق على أي اقتراح من نوع المجموع لـ Go †.

لنفترض أن لديك ثلاث واجهات A و B و C بالطرق A () و B () و C () على التوالي ، و Structure ABC بجميع الطرق الثلاثة. A و B و C مفككون لذا أ | ب | C وتباديلها كلها أنواع صالحة. لكن لا يزال لديك حالات مثل

var c C = ABC{}
var v A | B | C = c

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

ربما يجب أن يكون القيد هو أنه لا يمكن لأي من التلخيص أن يكون واجهات على الإطلاق؟

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

† لا يتضمن ذلك مُنشئات الأنواع للأنواع في المجموع لتوضيح الغموض (كما في Haskell حيث عليك أن تقول Just v لبناء قيمة من النوع ربما) - لكنني لست مع ذلك على الإطلاق.

jimmyfrasche هل حالة الاستخدام الخاصة بفتح الصندوق أمر مهم بالفعل؟ هذا ليس واضحًا بالنسبة لي ، وفي الحالات التي يكون فيها من السهل التعامل مع هياكل الصناديق الواضحة:

type ReadCloser struct {  io.ReadCloser }
type Reader struct { io.Reader }

var v ReadCloser | Reader = Reader{r}

bcmills إنه أكثر من ذلك أن النتائج ليست واضحة ومبهمة وتعني أن جميع الضمانات التي تريدها مع نوع المجموع تتبخر عندما تكون الواجهات متضمنة. أستطيع أن أرى أنه يسبب كل أنواع الأخطاء الخفية وسوء الفهم.

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

كل الضمانات التي تريدها بنوع المبلغ

يعتمد ذلك على الضمانات التي تتوقعها. أعتقد أنك تتوقع أن يكون نوع المجموع
قيمة ذات علامات صارمة ، لذا بالنظر إلى أي أنواع A | B | C ، فأنت تعرف بالضبط ما هو ثابت
اكتب التي عينتها لها. أراه بمثابة تقييد نوع على قيمة واحدة للخرسانة
النوع - القيد هو أن القيمة متوافقة مع نوع واحد (على الأقل) من A و B و C.
في النهاية ، إنها مجرد واجهة ذات قيمة.

أي ، إذا كان من الممكن تخصيص قيمة لنوع مجموع بحكم أنها متوافقة مع التخصيص
مع أحد أعضاء نوع المجموع ، لا نسجل أي من هؤلاء الأعضاء كان
"المختار" - نقوم فقط بتسجيل القيمة نفسها. نفس الشيء عند تعيين io.Reader
إلى واجهة {} ، تفقد نوع io.Reader الثابت ولديك القيمة نفسها فقط
وهو متوافق مع io.Reader ولكن أيضًا مع أي نوع واجهة آخر يحدث
لتنفيذ.

في مثالك:

var c C = ABC{}
var v A | B | C = c

سينجح تأكيد نوع v لأي من A و B و C. الذي يبدو معقولا بالنسبة لي.

rogpeppe هذه الدلالات منطقية أكثر مما كنت

لنفترض أن لديك type U I | *T حيث I هو نوع واجهة و *T هو نوع يستخدم I .

منح

var i I = new(T)
var u U = i

النوع الديناميكي لـ u هو *T ، وفي

var u U = new(T)

يمكنك الوصول إلى هذا *T باعتباره I مع تأكيد النوع. هل هذا صحيح؟

قد يعني ذلك أن التخصيص من قيمة واجهة صالحة إلى مبلغ يجب أن يبحث عن أول نوع مطابق في المجموع.

سيكون أيضًا مختلفًا إلى حد ما عن شيء مثل var v uint8 | int32 | int64 = i والذي أتخيله ، فقط استخدم دائمًا أيًا من هذه الأنواع الثلاثة i حتى لو كان i int64 يمكن أن يتناسب مع uint8 .

تقدم!

ياي!

يمكنك الوصول إلى هذا * T كـ I مع تأكيد النوع. هل هذا صحيح؟

نعم فعلا.

قد يعني ذلك أن التخصيص من قيمة واجهة صالحة إلى مبلغ يجب أن يبحث عن أول نوع مطابق في المجموع.

نعم ، كما يقول الاقتراح (بالطبع يعرف المترجم بشكل ثابت أي واحد يختار لذلك لا يوجد بحث في وقت التشغيل).

سيكون أيضًا مختلفًا إلى حد ما عن شيء مثل var v uint8 | int32 | int64 = أنا الذي أتخيله ، فقط أذهب دائمًا مع أي من هذه الأنواع الثلاثة أنا حتى لو كنت int64 الذي يمكن أن يتناسب مع uint8.

نعم ، لأنه ما لم يكن i ثابتًا ، فسيتم تخصيصه لواحد من تلك البدائل فقط.

نعم ، لأنه ما لم يكن i ثابتًا ، فسيتم تخصيصه لواحد من تلك البدائل فقط.

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

لذا فإن النوع I | *T من آخر مشاركة لي هو فعليًا نفس النوع I و io.ReadCloser | io.Reader هو فعليًا نفس النوع مثل io.Reader ؟

هذا صحيح. سيتم تغطية كلا النوعين من خلال قاعدتي المقترحة بأن المترجم يرفض أنواع المجموع حيث يكون أحد الأنواع واجهة يتم تنفيذها بواسطة نوع آخر. يمكن أن تغطي نفس القاعدة أو ما شابه أنواع المجموع بأنواع مكررة مثل int|int .

فكرة واحدة: ربما يكون من غير المنطقي أن int|byte ليس هو نفسه byte|int ، لكن من المحتمل أن يكون ذلك جيدًا في الممارسة.

قد يعني ذلك أن التخصيص من قيمة واجهة صالحة إلى مبلغ يجب أن يبحث عن أول نوع مطابق في المجموع.

نعم ، كما يقول الاقتراح (بالطبع يعرف المترجم بشكل ثابت أي واحد يختار لذلك لا يوجد بحث في وقت التشغيل).

أنا لا أتابع هذا. الطريقة التي قرأتها بها (والتي قد تكون مختلفة عما كان مقصودًا) هناك طريقتان على الأقل للتعامل مع اتحاد U من I و T-implements-I.

1a) عند تكليف U u = t ، تم تعيين العلامة على T. ينتج لاحقًا عن التحديد T لأن العلامة هي T.
1b) عند تكليف U u = i (أنا حقًا حرف T) ، تم تعيين العلامة على I. ينتج عن التحديد اللاحق T لأن العلامة هي I ولكن التحقق الثاني (يتم إجراؤها لأن T تنفذ I و T هو عضو في U) يكتشف T.

2 أ) مثل 1 أ
2 ب) عند التخصيص U u = i (أنا حقًا حرف T) ، يتحقق الكود المُنشأ من القيمة (i) لمعرفة ما إذا كانت في الواقع T ، لأن T تنفذ I و T هي أيضًا عضو في U. لأن يتم تعيين العلامة على T.

في حالة قيام T و V و W بتنفيذ I و U = *T | *V | *W | I ، تتطلب المهمة U u = i (حتى) 3 اختبارات من النوع.

لم تكن الواجهات والمؤشرات هي حالة الاستخدام الأصلية لأنواع الاتحاد ، رغم ذلك ، أليس كذلك؟

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

أو إذا كان لدينا مساحة عنوان من 50 بت ، وكنا على استعداد لأخذ بعض الحريات مع NaNs ، فيمكننا وضع الأعداد الصحيحة والمؤشرات والمضاعفات كلها في اتحاد 64 بت ، والتكلفة المحتملة لبعض العبث.

كلا الاقتراحين الفرعيين مقسمان ، وأنا متأكد من أن كلاهما سيكون له عدد صغير (؟) من المؤيدين المتعصبين.

وهذا بدوره يعني أنه ليس من الجيد أخذ عنوان عضو نقابة

صيح. لكنني لا أعتقد أن نتيجة تأكيد النوع يمكن معالجتها اليوم على أي حال ، أليس كذلك؟

عند تعيين U u = i (أنا حقًا حرف T) ، يتم تعيين العلامة على I.

أعتقد أن هذا هو الجوهر - لا توجد علامة I.

تجاهل تمثيل وقت التشغيل للحظة واعتبر نوع المجموع كواجهة. كما هو الحال مع أي واجهة ، لها نوع ديناميكي (النوع المخزن فيها). "العلامة" التي تشير إليها هي بالضبط ذلك النوع الديناميكي.

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

لم تكن الواجهات والمؤشرات هي حالة الاستخدام الأصلية لأنواع الاتحاد ، رغم ذلك ، أليس كذلك؟

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

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

حتى لو احتوى نوع المجموع على أنواع محددة فقط وتم تنفيذه مثل اتحاد مميز على النمط C ، فلن تتمكن من معالجة قيمة في نوع المجموع لأن هذا العنوان يمكن أن يصبح نوعًا مختلفًا (وحجمًا) بعد أن أخذت العنوان. ومع ذلك ، يمكنك أن تأخذ عنوان القيمة المكتوبة بالمجموع نفسه.

هل من المستحسن أن تتصرف أنواع المجموع بهذه الطريقة؟ يمكننا أن نعلن بسهولة أن النوع المحدد / المؤكد هو نفس ما قاله المبرمج / ضمنيًا عندما تم تعيين قيمة إلى الاتحاد. وإلا فقد يتم توجيهنا إلى أماكن مثيرة للاهتمام فيما يتعلق بـ int8 مقابل int16 مقابل int32 ، إلخ. أو ، على سبيل المثال ، int8 | uint8 .

هل من المستحسن أن تتصرف أنواع المجموع بهذه الطريقة؟

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

وإلا فقد يتم توجيهنا إلى أماكن مثيرة للاهتمام فيما يتعلق بـ int8 مقابل int16 مقابل int32 ، إلخ. أو ، على سبيل المثال ، int8 | uint8.

ما هو قلقك هنا؟

لا يمكنك استخدام نوع الوظيفة كنوع مفتاح الخريطة. أنا لا أقول أن هذا مكافئ ، فقط أن هناك سابقة لأنواع تقيد أنواعًا أخرى. لا يزال مفتوحًا للسماح بالواجهات التي لم تُباع بعد.

ما نوع البرامج التي يمكنك كتابتها بنوع مجموع يحتوي على واجهات لا يمكنك كتابتها بطريقة أخرى؟

اقتراح مضاد.

نوع الاتحاد هو نوع لا يسرد أنواعًا أو أكثر ، مكتوبة

union {
  T0
  T1
  //...
  Tn
}

يجب أن تكون جميع الأنواع المدرجة (T0 ، T1 ، ... ، Tn) في الاتحاد مختلفة ولا يمكن أن يكون أي منها من أنواع الواجهة.

يمكن إعلان الأساليب على نوع اتحاد محدد (مسمى) بالقواعد المعتادة. لا توجد طرق يتم الترويج لها من الأنواع المدرجة.

لا يوجد تضمين لأنواع الاتحاد. إدراج نوع اتحاد واحد في آخر مماثل لإدراج أي نوع صالح آخر. ومع ذلك ، لا يمكن للاتحاد سرد نوعه الخاص به بشكل متكرر ، لنفس السبب الذي يجعل type S struct { S } غير صالح.

يمكن دمج النقابات في هياكل.

قيمة نوع الاتحاد هي نوع ديناميكي ، يقتصر على أحد الأنواع المدرجة ، وقيمة من النوع الديناميكي - يقال إنها القيمة المخزنة. أحد الأنواع المدرجة بالضبط هو النوع الديناميكي في جميع الأوقات.

القيمة الصفرية للوحدة الفارغة فريدة. القيمة الصفرية لاتحاد غير فارغ هي القيمة الصفرية للنوع الأول المدرج في الاتحاد.

يمكن إنشاء قيمة لنوع الاتحاد ، U ، باستخدام U{} للقيمة الصفرية. إذا كان U يحتوي على نوع واحد أو أكثر وكان v قيمة أحد الأنواع المدرجة ، فإن T ، U{v} ينشئ قيمة اتحاد تخزن v بنوع ديناميكي T . إذا كان v من نوع غير مدرج في U والذي يمكن تخصيصه لأكثر من نوع من الأنواع المدرجة ، يلزم إجراء تحويل صريح لإزالة الغموض.

يمكن تحويل قيمة نوع الاتحاد U إلى نوع اتحاد آخر V كما في V(U{}) إذا كانت مجموعة الأنواع في U مجموعة فرعية من مجموعة من الأنواع في V . أي ، تجاهل الطلب ، يجب أن يحتوي U على نفس الأنواع مثل V ، و U لا يمكن أن يحتوي على أنواع ليست في V لكن V يمكن أن يحتوي U .

يتم تعريف التخصيص بين أنواع الاتحاد على أنه قابلية التحويل ، طالما أن أحد أنواع الاتحاد محدد (مسمى) على الأكثر.

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

إذا كانت جميع الأنواع المدرجة تدعم عوامل تشغيل المساواة:

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

لا يتم دعم أي عوامل تشغيل أخرى على قيم من نوع الاتحاد.

يتم تعليق تأكيد النوع مقابل نوع الاتحاد لأحد الأنواع المدرجة إذا كان النوع المؤكد هو النوع الديناميكي.

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

يجب أن تكون مفاتيح التبديل إما شاملة ، بما في ذلك جميع الأنواع المدرجة ، أو تحتوي على حالة افتراضية.

ترجع تأكيدات الكتابة ومفاتيح الكتابة نسخة من القيمة المخزنة.

يتطلب انعكاس الحزمة طريقة للحصول على النوع الديناميكي والقيمة المخزنة لقيمة الاتحاد المنعكسة وطريقة للحصول على الأنواع المدرجة لنوع الاتحاد المنعكس.

ملحوظات:

تم اختيار بناء الجملة union{...} جزئيًا للتمييز عن اقتراح نوع المجموع في هذا الموضوع ، بشكل أساسي للاحتفاظ بالخصائص الجميلة في قواعد Go ، وبالمناسبة لتعزيز أن هذا اتحاد مميز. نتيجة لذلك ، يتيح هذا اتحادات غريبة نوعًا ما مثل union{} و union{ int } . الأول في كثير من النواحي يعادل struct{} (على الرغم من أنه نوع مختلف حسب التعريف) لذلك لا يضيف إلى اللغة ، بخلاف إضافة نوع فارغ آخر. ربما يكون الثاني أكثر فائدة. على سبيل المثال ، type Id union { int } يشبه إلى حد كبير type Id struct { int } باستثناء أن إصدار الاتحاد يسمح بالتعيين المباشر دون الحاجة إلى تحديد idValue.int مما يسمح له أن يبدو أشبه بنوع مضمن.

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

عدم التضمين هو نتيجة للسماح بالطرق على النقابات وتتطلب مطابقة شاملة في مفاتيح النوع.

إن السماح بالطرق على الاتحاد نفسه بدلاً من أخذ التقاطع الصحيح لطرق الأنواع المدرجة يتجنب الحصول عن طريق الخطأ على طرق غير مرغوب فيها. يسمح النوع الذي يؤكد القيمة المخزنة للواجهات الشائعة بطرق غلاف بسيطة وواضحة عندما يكون الترويج مطلوبًا. على سبيل المثال ، في نوع الاتحاد U كل الأنواع المدرجة تطبق fmt.Stringer :

func (u U) String() string {
  return u.(fmt.Stringer).String()
}

في خيط reddit المرتبط ، قال rsc:

سيكون من الغريب أن تكون القيمة الصفرية لمجموع {X؛ Y} تختلف عن المجموع {Y؛ X}. هذه ليست الطريقة التي تعمل بها المبالغ عادة.

لقد كنت أفكر في هذا ، لأنه ينطبق على أي اقتراح حقًا.

هذا ليس خطأ: إنها ميزة.

انصح

type (
  Undefined = struct{}
  UndefinedOrInt union { Undefined; int }
)

ضد.

type (
  Illegal = struct{}
  IntOrIllegal union { int; Illegal }
)

UndefinedOrInt افتراضيًا أنه لم يتم تعريفه بعد ، ولكن عندما يتم ذلك ، ستكون قيمة int . هذا مشابه لـ *int وهو كيف يجب تمثيل نوع المجموع (1 + int) في Go now وقيمة الصفر مماثلة أيضًا.

من ناحية أخرى ، يقول IntOrIllegal بشكل افتراضي أنه الرقم 0 int ، ولكن قد يتم تمييزه في وقت ما على أنه غير قانوني. لا يزال هذا مماثلاً لـ *int لكن القيمة الصفرية أكثر تعبيرًا عن النية ، مثل فرض أنها افتراضية على new(int) .

يشبه الأمر إلى حد ما القدرة على صياغة حقل منطقي في بنية بالسالب ، لذا فإن القيمة الصفرية هي ما تريده كقيمة افتراضية.

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

إذا كان المجموع عبارة عن أيام من تعداد الأسبوع (كل يوم يتم تحديده struct{} ) ، أيهما يتم إدراجه أولاً هو اليوم الأول من الأسبوع ، نفس الشيء بالنسبة لتعداد نمط iota .

أيضًا ، لست على دراية بأي لغة بها أنواع مجموع أو اتحادات مميزة / مميزة لها مفهوم القيمة الصفرية. سيكون C هو الأقرب ولكن القيمة الصفرية هي ذاكرة غير مهيأة - بالكاد يكون هناك دليل يجب اتباعه. أعتقد أن Java افتراضية هي null ، ولكن هذا لأن كل شيء هو مرجع. جميع اللغات الأخرى التي أعرفها لديها مُنشئات أنواع إلزامية للتجمعات ، لذلك لا يوجد بالفعل مفهوم للقيمة الصفرية. هل توجد مثل هذه اللغة؟ ماذا تعمل، أو ماذا تفعل؟

إذا كان الاختلاف عن المفاهيم الرياضية لـ "الجمع" و "الاتحاد" هو المشكلة ، فيمكننا دائمًا تسميتها شيئًا آخر (مثل "البديل").

للأسماء: الاتحاد يخلط بين الأصوليين c / c ++. المتغير مألوف بشكل أساسي لمبرمجي COBRA و COM حيث يبدو أن اللغات الوظيفية مفضلة كنقابة تمييزية. المجموعة هي فعل واسم. تعجبني الكلمة الأساسية _pick_. استخدم Limbo _pick_. إنه قصير ويصف نية النوع للاختيار من بين مجموعة محدودة من الأنواع.

الاسم / بناء الجملة غير ذي صلة إلى حد كبير. اختيار سيكون على ما يرام.

يناسب أي من الاقتراحين في هذا الموضوع التعريف النظري المحدد.

يعتبر النوع الأول خاصًا للقيمة الصفرية غير ذي صلة منذ كتابة المجاميع النظرية ، وبالتالي فإن الترتيب غير ذي صلة (A + B = B + A). يحافظ اقتراحي على هذه الخاصية ، لكن أنواع المنتجات أيضًا تنتقل نظريًا وتعتبر مختلفة في الممارسة من قبل معظم اللغات (Go المدرجة ،) لذلك ربما لا تكون ضرورية.

تضمين التغريدة

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

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

rogpeppejimmyfrasche شيء لا أراه في مقترحاتك هو سبب قيامنا بذلك. هناك عيب واضح لإضافة نوع جديد من النوع: إنه مفهوم جديد يجب على كل من يميل إلى Go أن يتعلمه. ما هي الميزة التعويضية؟ على وجه الخصوص ، ما الذي يمنحنا إياه النوع الجديد ولا نحصل عليه من أنواع الواجهات؟

لخصianlancetaylor روبرت الأمر جيدًا هنا: https://github.com/golang/go/issues/19412#issuecomment -288608089

تضمين التغريدة
في نهاية اليوم ، يجعل الكود أكثر قابلية للقراءة ، وهذا هو التوجيه الأساسي لـ Go. ضع في اعتبارك json.Token ، فهو مُعرَّف حاليًا كواجهة {} ، ومع ذلك تشير الوثائق إلى أنه يمكن أن يكون في الواقع نوعًا واحدًا فقط من عدد محدد من الأنواع. من ناحية أخرى ، إذا تم كتابته كـ

type Token Delim | bool | float64 | Number | string | nil

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

في نهاية اليوم ، يجعل الكود أكثر قابلية للقراءة ، وهذا هو التوجيه الأساسي لـ Go.

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

تضمين التغريدة

المزيد من الميزات يعني أن على المرء معرفة المزيد لفهم الكود.

ليس دائما. إذا كان بإمكانك استبدال "معرفة المزيد عن اللغة" بـ "معرفة المزيد عن الثوابت سيئة التوثيق أو غير المتسقة في الكود" ، فقد يظل ذلك بمثابة ربح صافٍ. (أي أن المعرفة العالمية يمكن أن تحل محل الحاجة إلى المعرفة المحلية).

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

//vet:types Delim | bool | float64 | Number | string | nil
type Token interface{}

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

يمكن إضافة العديد من الاختبارات التي أجراها cmd / vet إلى اللغة ، بمعنى أنه يمكن فحصها بواسطة المترجم بدلاً من أداة تحليل ثابتة منفصلة. ولكن لأسباب مختلفة نجد أنه من المفيد فصل الطبيب البيطري عن المترجم. لماذا يقع هذا المفهوم في جانب اللغة بدلاً من جانب الطبيب البيطري؟

راجعianlancetaylor التعليقات: https://github.com/BurntSushi/go-sumtype

ianlancetaylor فيما يتعلق بما إذا كان التغيير مبررًا أم لا ، كنت

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

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

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

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

تضمين التغريدة

لماذا يقع هذا المفهوم في جانب اللغة بدلاً من جانب الطبيب البيطري؟

يمكنك تطبيق نفس السؤال على أي ميزة خارج نواة turing-Complete ، ويمكن القول أننا لا نريد أن نكون "لوحة مفاتيح turing". من ناحية أخرى، ونحن لدينا أمثلة من اللغات التي يشق فرعية هامة للغة الفعلية قبالة إلى تركيب عام "تمديد". (على سبيل المثال ، "السمات" في Rust و C ++ و GNU C.)

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

في سياق Go ، يبدو أن السبب الرئيسي لوضع الميزات في vet هو تنفيذ التغييرات التي لن تحافظ على توافق Go 1 إذا تم تطبيقها على اللغة نفسها. لا أرى أن هذا يمثل مشكلة هنا.

أحد أسباب عدم وضع الميزات في vet هو الحاجة إلى نشرها أثناء التجميع. على سبيل المثال ، إذا كتبت:

switch x := somepkg.SomeFunc().(type) {
…
}

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

@ dr2chase بشكل عام ، بالطبع ، أنت محق ، لكن هل أنت محق في هذا المثال المحدد؟ الشفرة مفهومة تمامًا دون معرفة معنى التعليق السحري. التعليق السحري لا يغير ما يفعله الكود بأي شكل من الأشكال. يجب أن تكون رسائل الخطأ من الطبيب البيطري واضحة.

تضمين التغريدة

لماذا يقع هذا المفهوم في جانب اللغة بدلاً من جانب الطبيب البيطري؟

يمكنك تطبيق نفس السؤال على أي ميزة خارج نواة turing-Complete ....

أنا لا أوافق. إذا كانت الميزة قيد المناقشة تؤثر على الكود المترجم ، فهناك حجة تلقائية لصالحها. في هذه الحالة ، يبدو أن الميزة لا تؤثر على الكود المترجم.

(ونعم ، يمكن للطبيب البيطري تحليل مصدر الحزم المستوردة.)

أنا لا أحاول أن أدعي أن حجتي حول الطبيب البيطري قاطعة. لكن كل تغيير لغة يبدأ من موقف سلبي: لغة بسيطة أمر مرغوب فيه للغاية ، وميزة جديدة مهمة مثل هذه تجعل اللغة حتمًا أكثر تعقيدًا. أنت بحاجة إلى حجج قوية لصالح تغيير اللغة. ومن وجهة نظري هذه الحجج القوية لم تظهر بعد. بعد كل شيء ، لقد فكرنا في هذه المشكلة لفترة طويلة ، وهي أسئلة شائعة (https://golang.org/doc/faq#variant_types).

تضمين التغريدة

في هذه الحالة ، يبدو أن الميزة لا تؤثر على الكود المترجم.

أعتقد أن هذا يعتمد على التفاصيل المحددة؟ "القيمة الصفرية للمجموع هي القيمة الصفرية للنوع الأول" التي ذكرها jimmyfrasche أعلاه (https://github.com/golang/go/issues/19412#issuecomment-289319916) بالتأكيد.

urandom كنت أكتب شرحًا مطولًا لسبب عدم اختلاط أنواع الواجهة

اقتراح مضاد سريع وقذر لاقتراحي المضاد. (أي شيء لم يذكر صراحة هو نفس اقتراحي السابق). لست متأكدًا من أن أحد المقترحات أفضل من الآخر ، لكن هذا الاقتراح يسمح بالواجهات وهو أكثر وضوحًا في كل مكان:

الاتحاد له "أسماء حقول" صريحة تسمى فيما بعد "أسماء العلامات":

union { //or whatever
  None, invalid struct{} //None is zero value
  Good, Bad int
  Err error //okay because it's explicitly named
}

لا يوجد حتى الآن التضمين. من الخطأ دائمًا أن يكون لديك نوع بدون اسم علامة.

تحتوي قيم الاتحاد على علامة ديناميكية بدلاً من نوع ديناميكي.

إنشاء القيمة الحرفية: U{v} صالح فقط إذا كان واضحًا تمامًا ، وإلا يجب أن يكون U{Tag: v} .

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

التنازل عن النقابة ليس سحرًا. يعني دائمًا تخصيص قيمة اتحاد متوافقة. لتعيين القيمة المخزنة ، يجب استخدام اسم العلامة المطلوب بشكل صريح: يعيّن v.Good = 1 العلامة الديناميكية إلى Good والقيمة المخزنة على 1.

يستخدم الوصول إلى القيمة المخزنة تأكيد العلامة بدلاً من تأكيد النوع:

g := v.[Tag] //may panic
g, ok := v.[Tag] //no panic but could return zero-value, false

v العلامة خطأ في rhs لأنها غامضة.

مفاتيح التبديل هي مثل مفاتيح النوع ، مكتوبة switch v.[type] ، باستثناء أن الحالات هي علامات الاتحاد.

تصمد تأكيدات النوع فيما يتعلق بنوع العلامة الديناميكية. مفاتيح الكتابة تعمل بالمثل.

إعطاء القيم a ، b من نوع الاتحاد ، a == b إذا كانت علاماتها الديناميكية هي نفسها والقيمة المخزنة هي نفسها.

يتطلب التحقق مما إذا كانت القيمة المخزنة قيمة معينة تأكيد علامة.

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

يحتاج الانعكاس إلى التعامل مع أسماء العلامات أيضًا.

هـ: توضيح للنقابات المتداخلة. منح

type U union {
  A union {
    A1 T1
    A2 T2
  }
  B union {
    B1 T3
    B2 T4
  }
}
var u U

قيمة u هي العلامة الديناميكية A والقيمة المخزنة هي الاتحاد المجهول مع العلامة الديناميكية A1 وقيمتها المخزنة هي القيمة الصفرية لـ T1.

u.B.B2 = returnsSomeT3()

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

v := u.[A].[A2]

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

تحرير 2: توضيح بشأن تأكيدات النوع.

منح

type U union {
  Exported, unexported int
}
var u U

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

ianlancetaylor فيما يلي بعض الأمثلة عن كيفية مساعدة هذه الميزة:

  1. في قلب بعض الحزم ( go/ast على سبيل المثال) يوجد نوع واحد أو أكثر من أنواع المبالغ الكبيرة. من الصعب التنقل في هذه الحزم دون فهم هذه الأنواع. الأمر الأكثر إرباكًا هو أنه في بعض الأحيان يتم تمثيل نوع المجموع بواجهة ذات طرق (على سبيل المثال go/ast.Node ) ، وفي أحيان أخرى بالواجهة الفارغة (على سبيل المثال go/ast.Object.Decl ).

  2. ينتج عن تجميع ميزة protobuf oneof للانتقال إلى نوع واجهة غير مُصدَّر والغرض الوحيد منه هو التأكد من أن التعيين إلى حقل واحد من النوع الآمن. وهذا بدوره يتطلب إنشاء نوع لكل فرع من فروع. من الصعب قراءة وكتابة الأحرف الحرفية للمنتج النهائي:

    &sppb.Mutation{
               Operation: &sppb.Mutation_Delete_{
                   Delete: &sppb.Mutation_Delete{
                       Table:  m.table,
                       KeySet: keySetProto,
                   },
               },
    }
    

    يمكن التعبير عن بعض (وإن لم يكن كل) من خلال أنواع المجموع.

  3. أحيانًا يكون نوع "ربما" هو بالضبط ما يحتاجه المرء. على سبيل المثال ، تسمح العديد من عمليات تحديث موارد Google API بتغيير مجموعة فرعية من حقول المورد. إحدى الطرق الطبيعية للتعبير عن هذا في Go هي من خلال متغير بنية الموارد بنوع "ربما" لكل حقل. على سبيل المثال ، يبدو مورد Google Cloud Storage ObjectAttrs

    type ObjectAttrs struct {
       ContentType string
       ...
    }
    

    لدعم التحديثات الجزئية ، تحدد الحزمة أيضًا

    type ObjectAttrsToUpdate struct {
       ContentType optional.String
       ...
    }
    

    حيث يبدو optional.String هكذا (

    // String is either a string or nil.
    type String interface{}
    

    من الصعب تفسير هذا الأمر كما أنه غير آمن ، ولكن اتضح أنه مناسب من الناحية العملية ، لأن الحرف الحرفي ObjectAttrsToUpdate يبدو تمامًا مثل ObjectAttrs ، أثناء وجود الترميز. أتمنى لو كنا قد كتبنا

    type ObjectAttrsToUpdate struct {
       ContentType string | nil
       ...
    }
    
  4. تقوم العديد من الدوال بإرجاع (T, error) مع دلالات xor (T هو خطأ iff ذو مغزى هو لا شيء). كتابة نوع الإرجاع كـ T | error شأنه أن يوضح الدلالات ، ويزيد من الأمان ، ويوفر المزيد من الفرص للتكوين. حتى إذا لم نتمكن (لأسباب تتعلق بالتوافق) أو لا نريد تغيير قيمة إرجاع الدالة ، فإن نوع المجموع يظل مفيدًا لتحمل هذه القيمة ، مثل كتابتها إلى قناة.

من المسلم به أن التعليق التوضيحي go vet سيساعد العديد من هذه الحالات ، ولكن ليس الحالات التي يكون فيها النوع المجهول منطقيًا. أعتقد أنه إذا كان لدينا أنواع مجموع ، فسنرى الكثير منها

chan *Response | error

هذا النوع قصير بما يكفي للكتابة عدة مرات.

ianlancetaylor ربما لا تكون هذه بداية رائعة ، ولكن إليك كل ما يمكنك فعله مع النقابات والذي يمكنك القيام به بالفعل في Go1 ، لأنني اعتقدت أنه من العدل الاعتراف بهذه الحجج وتلخيصها:

(باستخدام أحدث اقتراحي مع علامات بناء الجملة / الدلالات أدناه. أيضًا بافتراض أن الكود المنبعث يشبه بشكل أساسي رمز C الذي نشرته في وقت سابق في سلسلة الرسائل.)

تتداخل أنواع المجموع مع ذرة المؤشرات والواجهات.

ذرة

هذان النوعان متكافئان تقريبًا:

type Stoplight union {
  Green, Yellow, Red struct {}
}

func (s Stoplight) String() string {
  switch s.[type] {
  case Green: return "green" //etc
  }
}

و

type Stoplight int

const (
  Green Stoplight = iota
  Yellow
  Red
)

func (s Stoplight) String() string {
  switch s {
  case Green: return "green" //etc
  }
}

من المحتمل أن يصدر المترجم نفس الكود لكليهما.

في إصدار الاتحاد ، يتم تحويل int إلى تفاصيل تنفيذ مخفية. باستخدام إصدار iota ، يمكنك أن تسأل ما هو Yellow / Red أو تعيين قيمة Stoplight إلى -42 ، ولكن ليس مع الإصدار الموحد - فهذه كلها أخطاء في المترجم وثوابت يمكن أخذها في الاعتبار أثناء التحسين. وبالمثل ، يمكنك كتابة مفتاح (قيمة) يفشل في حساب الأضواء الصفراء ولكن باستخدام مفتاح تبديل العلامات ، ستحتاج إلى حالة افتراضية لتوضيح ذلك.

بالطبع ، هناك أشياء يمكنك فعلها باستخدام iota لا يمكنك فعلها مع أنواع الاتحاد.

مؤشرات

هذان النوعان متكافئان تقريبًا

type MaybeInt64 union {
  None struct{}
  Int64 int64
}

و

type MaybeInt64 *int64

إصدار المؤشر أكثر إحكاما. ستحتاج نسخة الاتحاد إلى بت إضافي (والذي من المحتمل بدوره أن يكون بحجم الكلمة) لتخزين العلامة الديناميكية ، لذلك من المحتمل أن يكون حجم القيمة هو نفسه https://golang.org/pkg/database/sql/ # NullInt64

النسخة الموحدة توثق النية بشكل أكثر وضوحًا.

بالطبع ، هناك أشياء يمكنك القيام بها باستخدام المؤشرات التي لا يمكنك فعلها مع أنواع الاتحاد.

واجهات

هذان النوعان متكافئان تقريبًا

type AB union {
  A A
  B B
}

و

type AB interface {
  secret()
}
func (A) secret() {}
func (B) secret() {}

لا يمكن التحايل على النسخة الموحدة بالتضمين. لا يحتاج A و B إلى طرق مشتركة - فقد يكونان في الواقع نوعين بدائيين أو لديهما مجموعات طرق منفصلة تمامًا ، مثل json .

من السهل حقًا معرفة ما يمكنك وضعه في اتحاد AB مقابل واجهة AB: التعريف هو التوثيق (كان علي قراءة مصدر go / ast عدة مرات لمعرفة ما هو الشيء).

لا يمكن أبدًا أن يكون اتحاد AB صفريًا ويمكن إعطاؤه طرقًا خارج تقاطع مكوناته (يمكن محاكاة ذلك عن طريق تضمين الواجهة في بنية ولكن بعد ذلك يصبح البناء أكثر حساسية وعرضة للخطأ).

بالطبع ، هناك أشياء يمكنك القيام بها باستخدام الواجهات التي لا يمكنك القيام بها مع أنواع الاتحاد.

ملخص

ربما هذا التداخل هو الكثير من التداخل.

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

يمكن إنشاء إصدارات غير مألوفة من أمثلة ذرة المؤشر وأمثلةها باستخدام استراتيجية "الواجهة مع طريقة غير مُصدرة". لهذه المسألة ، على الرغم من ذلك ، يمكن محاكاة البنيات باستخدام واجهات map[string]interface{} و (غير فارغة) مع أنواع func وقيم الطريقة. لا أحد سيفعل ذلك لأنه أصعب وأقل أمانًا.

كل هذه الميزات تضيف شيئًا إلى اللغة ولكن يمكن التغلب على غيابها (بشكل مؤلم وفي ظل الاحتجاج).

لذلك أفترض أن الشريط لا يوضح برنامجًا لا يمكن حتى تقريبه في Go ، ولكن بدلاً من ذلك لإظهار برنامج يتم كتابته بسهولة وبصورة أكثر وضوحًا في Go with union وليس بدونه. إذن ما يتبقى هو ذلك.

تضمين التغريدة

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

في جوهرها ، يجب أن يبدو نوع النقابة الخاص بك كما يلي:

union {
    struct{}
    int
    err
}

ستوفر الأنواع نفسها معرفات بادئة فريدة يمكن استخدامها لتعيين اتحاد ، على غرار الطريقة التي يتم بها استخدام الأنواع المضمنة في الهياكل كمعرفات.

ومع ذلك ، لكي تعمل التعيينات الصريحة ، لا يمكن للمرء أن يكون قادرًا على إنشاء نوع اتحاد عن طريق تحديد نوع غير مسمى كعضو ، لأن بناء الجملة سيسمح بمثل هذا التعبير. على سبيل المثال ، v.struct{} = struct{}

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

الآن ، لست متأكدًا من النحو الذي سيكون أفضل.

[union|sum|pick|oneof] {
    type1
    package1.type2
    ....
}

ما ورد أعلاه يبدو أكثر شبهاً ، ولكنه مطول قليلاً لمثل هذا النوع.

من ناحية أخرى ، قد لا يبدو type1 | package1.type2 مثل نوع go المعتاد الخاص بك ، إلا أنه يكتسب فائدة استخدام "|" رمز ، والذي يتم التعرف عليه في الغالب على أنه OR. ويقلل الإسهاب دون أن يكون غامضًا.

urandom إذا لم يكن لديك "أسماء علامات" لكنك تسمح للواجهات بانهيار المجاميع إلى interface{} مع عمليات تحقق إضافية. لقد توقفوا عن كونهم أنواعًا مجمعة حيث يمكنك وضع شيء واحد ولكن يمكنك الحصول عليه بطرق متعددة. تسمح أسماء العلامات لهم بأن يكونوا من أنواع الجمع ويحملون واجهات بدون غموض.

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

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

type Counter union {
  Successes, Failures uint 
}

بدون أسماء العلامات التي تحتاجها

type (
  Success uint
  Failures uint
  Counter Successes | Failures
)

وستبدو المهمة مثل c = Successes(1) بدلاً من c.Successes = 1 . أنت لا تكسب الكثير.

مثال آخر هو نوع يمثل فشل محلي أو بعيد. من السهل نمذجة أسماء العلامات:

type Failure union {
  Local, Remote error
}

يمكن تحديد دليل الخطأ باسم العلامة ، بغض النظر عن الخطأ الفعلي. بدون أسماء العلامات ، ستحتاج إلى type Local { error } ونفس الشيء لجهاز التحكم عن بعد ، حتى لو سمحت بالواجهات مباشرة في المجموع.

تعد أسماء العلامات نوعًا من إنشاء أنواع خاصة لا اسم مستعار ولا أنواع مسماة محليًا في الاتحاد. إن وجود "علامات" متعددة بأنواع متطابقة ليس فريدًا في اقتراحي: إنه ما تفعله كل لغة وظيفية (أعرفها).

تعد القدرة على إنشاء علامات غير مُصدرة للأنواع المصدرة والعكس بالعكس تطورًا مثيرًا للاهتمام أيضًا.

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

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

للتوسع في ذلك إلى حد ما ، كان المثال المحفز بالنسبة لي من rogpeppe io.Reader | io.ReadCloser . السماح للواجهات بدون علامات ، هذا هو نفس النوع مثل io.Reader .

يمكنك وضع ReadCloser وإزالته كقارئ. تفقد A | تعني B خاصية A أو B لأنواع المجموع.

إذا كنت بحاجة إلى أن تكون محددًا بشأن التعامل مع io.ReadCloser أحيانًا كـ io.Reader فأنت بحاجة إلى إنشاء هياكل مجمعة كما أشار type Reader struct { io.Reader } وما إلى ذلك ويكون النوع Reader | ReadCloser .

حتى إذا قمت بتحديد المجاميع للواجهات ذات مجموعات الطرق المنفصلة ، فلا تزال لديك هذه المشكلة لأن أحد الأنواع يمكنه تنفيذ أكثر من واحدة من تلك الواجهات. تفقد صراحة أنواع المجموع: فهي ليست "A أو B": إنها "A أو B أو في بعض الأحيان أيهما تريد".

والأسوأ من ذلك ، إذا كانت هذه الأنواع من حزم أخرى ، فيمكنها فجأة التصرف بشكل مختلف بعد التحديث حتى لو كنت حريصًا جدًا على إنشاء برنامجك بحيث لا يتم التعامل مع A مثل B.

في الأصل اكتشفت عدم السماح بالواجهات لحل المشكلة. لم يكن أحد سعيدًا بذلك! لكنها أيضًا لم تتخلص من مشكلات مثل a = b تعني أشياء مختلفة اعتمادًا على نوعي a و b ، وهو ما لست مرتاحًا له. كان يجب أيضًا أن يكون هناك الكثير من القواعد حول النوع الذي يتم اختياره في الاختيار عندما يتم تشغيل إمكانية تعيين النوع. إنه كثير من السحر.

تضيف علامات ويختفي هذا كله.

باستخدام union { R io.Reader | RC io.ReadCloser } يمكنك القول صراحة أنني أريد أن يتم اعتبار ReadCloser هذا كقارئ إذا كان هذا منطقيًا. لا حاجة لأنواع المجمعات. إنه ضمني في التعريف. بغض النظر عن نوع العلامة ، فهي إما علامة واحدة أو أخرى.

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

أيضًا نظرًا لأن تأكيدات العلامة تختلف عن تأكيدات النوع ، إذا كنت لا تهتم حقًا وتريد فقط io.Reader بغض النظر ، يمكنك استخدام تأكيد النوع لسحب ذلك ، بغض النظر عن العلامة.

هذا هو أفضل ترجمة صوتية لمثال لعبة إلى Go بدون اتحادات / مبالغ / إلخ. ربما لا يكون هذا هو أفضل مثال ولكنه المثال الذي اعتدت أن أرى كيف سيبدو هذا.

يُظهر الدلالات بطريقة أكثر عملية ، والتي من المحتمل أن يكون فهمها أسهل من بعض النقاط النقطية المقتضبة في الاقتراح.

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

مواكبة مع اقتراح الاتحاد:

type fail union { //zero value: (Local, nil)
  Local, Remote error
}

func (f fail) Error() string {
  //Could panic if local/remote nil, but assuming
  //it will be constructed purposefully
  return f.(error).Error()
}

type U union { //zero value: (A, "")
  A, B, C string
  D, E    int
  F       fail
}

//in a different package

func create() pkg.U {
  return pkg.U{D: 7}
}

func process(u pkg.U) {
  switch u := u.[type] {
  case A:
    handleA(u) //undefined here, just doing something with unboxed value
  case B:
    handleB(u)
  case C:
    handleC(u)
  case D:
    handleD(u)
  case E:
    handleE(u)
  case F:
    switch u := u.[type] {
    case Local:
      log.Fatal(u)
    case Remote:
      log.Printf("remote error %s", u)
      retry()
    } 
  }
}

الترجمة الصوتية إلى Go الحالي:

(تم تضمين ملاحظات حول الاختلافات بين التحويل الصوتي وما ورد أعلاه)

const ( //simulates tags, namespaced so other packages can see them without overlap
  Fail_Local = iota
  Fail_Remote
)

//since there are only two tags with a single type this can
//be represented precisely and safely
//the error method on the full version of fail can be
//put more succinctly with type embedding in this case

type fail struct { //zero value (Fail_Local, nil) :)
  remote bool
  error
}

// e, ok := f.[Local]
func (f *fail) TagAssertLocal2() (error, bool) { //same for TagAssertRemote2
  if !f.remote {
    return nil, false
  }
  return f.error, true
}

// e := f.[Local]
func (f *fail) TagAssertLocal() error { //same for TagAssertRemote
  if !f.remote {
    panic("invalid tag assert")
  }
  return f.error
}

// f.Local = err
func (f *fail) SetLocal(err error) { //same for SetRemote
  f.remote = false
  f.error = err
}

// simulate tag switch
func (f *fail) TagSwitch() int {
  if f.remote {
    return Fail_Remote
  }
  return Fail_Local
}

// f.(someType) needs to be written as f.TypeAssert().(someType)
func (f *fail) TypeAssert() interface{} {
  return f.error
}

const (
  U_A = iota
  U_B
  // ...
  U_F
)

type U struct { //zero value (U_A, "", 0, fail{}) :(
  kind int //more than two types, need an int
  s string //these would all occupy the same space
  i int
  f fail
}

//s, ok := u.[A]
func (u *U) TagAssertA2() (string, bool) { //similar for B, etc.
  if u.kind == U_A {
    return u.s, true
  }
  return "", false
}

//s := u.[A]
func (u *U) TagAssertA() string { //similar for B, etc.
  if u.kind != U_A {
    panic("invalid tag assert")
  }
  return u.s
}

// u.A = s
func (u *U) SetA(s string) { //similar for B, etc.
  //if there were any pointers or reference types
  //in the union, they'd have to be nil'd out here,
  //since the space isn't shared
  u.kind = U_A
  u.s = s
}

// special case of u.F.Local = err
func (u *U) SetF_Local(err error) { //same for SetF_Remote
  u.kind = U_F
  u.f.SetLocal(err)
}

func (u *U) TagSwitch() int {
  return u.kind
}

func (u *U) TypeAssert() interface{} {
  switch u.kind {
  case U_A, U_B, U_C:
    return u.s
  case U_D, U_E:
    return u.i
  }
  return u.f
}

//in a different package

func create() pkg.U {
  var u pkg.U
  u.SetD(7)
  return u
}

func process(u pkg.U) {
  switch u.TagSwitch() {
  case U_A:
    handleA(u.TagAssertA())
  case U_B:
    handleB(u.TagAssertB())
  case U_C:
    handleC(u.TagAssertC())
  case U_D:
    handleD(u.TagAssertD())
  case U_E:
    handleE(u.TagAssertE())
  case U_F:
    switch u := u.TagAssertF(); u.TagSwitch() {
    case Fail_Local:
      log.Fatal(u.TagAssertLocal())
    case Fail_Remote:
      log.Printf("remote error %s", u.TagAssertRemote())
    }
  }
}

تضمين التغريدة

نظرًا لأن الاتحاد يحتوي على علامات قد يكون لها نفس النوع ، فلن تكون البنية التالية مناسبة بشكل أفضل:

func process(u pkg.U) {
  switch v := u {
  case A:
    handleA(v) //undefined here, just doing something with unboxed value
  case B:
    handleB(v)
  case C:
    handleC(v)
  case D:
    handleD(v)
  case E:
    handleE(v)
  case F:
    switch w := v {
    case Local:
      log.Fatal(w)
    case Remote:
      log.Printf("remote error %s", w)
      retry()
    } 
  }
}

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

أيضًا ، بموجب هذا الاقتراح ، هل سيكون هذا الرمز صالحًا:

type Foo union {
    // Completely different types, no ambiguity
    A string
    B int
}

func Bar(f Foo) {
    switch v := f {
        ....
    }
}

....

func main() {
    // No need for Bar(Foo{A: "hello world"})
    Bar("hello world")
    Bar(1)
}

urandom لقد اخترت بناء جملة لتعكس الدلالات باستخدام التشبيهات مع بناء جملة Go الحالي كلما أمكن ذلك.

مع أنواع واجهات يمكنك القيام بها

var i someInterface = someValue //where someValue implements someInterface.
var j someInterface = i //this assignment is different from the last one.

هذا جيد ولا لبس فيه لأنه لا يهم نوع someValue طالما تم إرضاء العقد.

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

لا أرى فائدة في أن أكون قادرًا في بعض الأحيان على تخطي خطوة ، خاصةً عندما يمكن أن يؤدي تغيير الكود إلى إبطال هذه الحالة الخاصة بسهولة ، ثم يتعين عليك العودة وتحديث كل التعليمات البرمجية على أي حال. لاستخدام مثال Foo / Bar إذا تمت إضافة C int إلى Foo ثم يجب تغيير Bar(1) ولكن ليس Bar("hello world") . إنه يعقد كل شيء لحفظ بضع ضغطات على المفاتيح في المواقف التي قد لا تكون شائعة جدًا ويجعل من الصعب فهم المفاهيم لأنها تبدو أحيانًا هكذا وأحيانًا تبدو كذلك - فقط استشر هذا المخطط الانسيابي السهل لمعرفة أيهما ينطبق عليك!

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

بمعنى ما ، النقابات الموسومة تشبه البنية أكثر من الواجهة. إنها نوع خاص من الهياكل التي لا يمكن أن يكون لها سوى حقل واحد في كل مرة. في ضوء ذلك ، سيكون مثال Foo / Bar الخاص بك مثل قول هذا:

type Foo struct {
  A string
  B int
}

func Bar(f Foo) {...}

func main() {
  Bar("hello world") //same as Bar(Foo{A: "hello world", B: 0})
  Bar(1) //same as Bar(Foo{A: "", B: 1})
}

في حين أنه لا لبس فيه في هذه الحالة ، لا أعتقد أنها فكرة جيدة.

أيضًا في الاقتراح يُسمح بـ Bar(Foo{1}) عندما يكون واضحًا إذا كنت تريد حقًا حفظ ضغطات المفاتيح. يمكنك أيضًا الحصول على مؤشرات للنقابات ، لذا لا يزال التركيب الحرفي المركب ضروريًا لـ &Foo{"hello world"} .

ومع ذلك ، فإن النقابات لديها تشابه مع الواجهات من حيث أن لديها علامة ديناميكية تم تعيين "الحقل" لها حاليًا.

يعكس switch v := u.[type] {... بشكل جيد switch v := i.(type) {... للواجهات بينما لا يزال يسمح بمفاتيح التبديل والتأكيدات مباشرة على قيم الاتحاد. ربما يجب أن يكون u.[union] لتسهيل تحديده ، ولكن في كلتا الحالتين ، فإن بناء الجملة ليس ثقيلًا ومن الواضح ما هو المقصود.

يمكنك تقديم نفس الحجة القائلة بأن .(type) غير ضروري ، ولكن عندما ترى أنك تعرف دائمًا ما يحدث بالضبط وهذا يبرر ذلك تمامًا ، في رأيي.

كان هذا هو المنطق وراء هذه الخيارات.

تضمين التغريدة
يبدو بناء جملة التبديل غير بديهي بعض الشيء بالنسبة لي ، حتى بعد توضيحاتك. باستخدام الواجهة ، يقوم switch v := i.(type) {... بالتبديل بين الأنواع الممكنة ، كما هو موضح في حالات التبديل ، ويشار إليه بـ .(type) .
ومع ذلك ، مع الاتحاد ، لا يقوم المحول بالتبديل بين الأنواع الممكنة ، ولكن القيم. تمثل كل حالة قيمة مختلفة ممكنة ، حيث قد تشترك القيم في نفس النوع. هذا يشبه إلى حد كبير السلاسل ومفاتيح التحويل ، حيث تسرد الحالات أيضًا القيم ، ويكون تركيبها بسيطًا switch v := u {... . من ذلك ، يبدو لي أنه من الطبيعي أن يكون التبديل بين قيم الاتحاد switch v := u { ... ، لأن الحالات متشابهة ، ولكنها أكثر تقييدًا ، من حالات ints و Strings.

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

switch u {... ستنجح لكن المشكلة مع switch v := u {... هي أنها تبدو مثل switch v := f(); v {... (مما يجعل الإبلاغ عن الأخطاء أكثر صعوبة - ليس من الواضح ما هو المقصود).

إذا تمت إعادة تسمية الكلمة الرئيسية union إلى pick كما تم اقتراحه بواسطة as ، فيمكن كتابة مفتاح العلامة كـ switch u.[pick] {... أو switch v := u.[pick] {... الذي يحافظ على التناظر مع نوع التبديل لكنه يفقد الارتباك ويبدو جميلًا.

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

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

تحرير: هذا من شأنه أن يجعل استخدام انعكاس مع اللقطات أمرًا محرجًا ، على الرغم من ذلك

[آسف لتأخر الرد - كنت بعيدًا في إجازة]

كتب ianlancetaylor :

هناك شيء لا أراه في مقترحاتك هو لماذا يجب علينا القيام بذلك. هناك عيب واضح لإضافة نوع جديد من النوع: إنه مفهوم جديد يجب على كل من يميل إلى Go أن يتعلمه. ما هي الميزة التعويضية؟ على وجه الخصوص ، ما الذي يمنحنا إياه النوع الجديد ولا نحصل عليه من أنواع الواجهات؟

هناك ميزتان رئيسيتان أراهما. الأول هو ميزة اللغة. والثاني هو ميزة الأداء.

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

  • هناك أوقات يوجد فيها عدد قليل من أنواع الرسائل المحتملة المعروفة ، ولا يحتوي أي منها على مؤشرات. إذا استخدمنا واجهة مفتوحة لتمثيلها ، فإننا نحتاج إلى تحمل تخصيص لعمل قيم للواجهة. إن استخدام نوع يقيد أنواع الرسائل المحتملة يعني أنه يمكن تجنبها وبالتالي تخفيف ضغط GC وزيادة موقع ذاكرة التخزين المؤقت.

ألم خاص بالنسبة لي يمكن أن تحلّه أنواع المجموع هو godoc. خذ ast.Spec على سبيل المثال: https://golang.org/pkg/go/ast/#Spec

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

إذا كانت اللغة تعرف بالفعل جميع القيم الممكنة ، فيمكن أتمتة ذلك في godoc مثل أنواع التعداد مع iotas. يمكنهم أيضًا ربط الأنواع ، بدلاً من كونها مجرد نص عادي.

تحرير: مثال آخر: https://github.com/mvdan/sh/commit/ebbfda50dfe167bee741460a4491ffec1006bdef

mvdan هذه نقطة ممتازة وعملية لتحسين القصة في Go1 دون أي تغييرات لغوية. هل يمكنك تقديم مشكلة منفصلة لذلك والرجوع إليها؟

عذرًا ، هل تشير فقط إلى روابط لأسماء أخرى داخل صفحة godoc ، ولكن ما زلت تدرجها يدويًا؟

آسف ، كان ينبغي أن يكون أوضح.

قصدت طلب ميزة للتعامل تلقائيًا مع الأنواع التي تنفذ واجهات محددة في الحزمة الحالية في godoc.

(أعتقد أن هناك طلب ميزة في مكان ما لربط الأسماء المدرجة يدويًا ، لكن ليس لدي الوقت لمطاردتها في الوقت الحالي).

لا أرغب في تولي هذا الموضوع (الطويل جدًا بالفعل) ، لذلك قمت بإنشاء مشكلة منفصلة - انظر أعلاه.

Merovius أقوم بالرد على https://github.com/golang/go/issues/19814#issuecomment -298833986 في هذه المشكلة نظرًا لأن عناصر AST تنطبق أكثر على أنواع الجمع أكثر من التعداد. نعتذر عن دفعك إلى مشكلة أخرى.

أولاً ، أود أن أكرر أنني لست متأكدًا مما إذا كانت أنواع المجموع تنتمي إلى Go. لم أقنع نفسي بعد أنهم بالتأكيد لا ينتمون. أنا أعمل على افتراض أنهم يفعلون ذلك من أجل استكشاف الفكرة ومعرفة ما إذا كانت مناسبة. أنا على استعداد لأن أكون مقتنعًا في كلتا الحالتين.

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

لنفكر في مثال AST ، الذي تم تنفيذه بواجهة Node ، والذي يضيف نوعًا جديدًا من العقدة. لنفترض أن AST تم تعريفه في مشروع خارجي وأنك تقوم باستيراده في حزمة في مشروعك ، والتي تمشي في AST.

هناك عدد من الحالات:

  1. تتوقع شفرتك أن تسير في كل عقدة:
    1.1 ليس لديك إفادة افتراضية ، فالشفرة الخاصة بك غير صحيحة بصمت
    1.2 لديك عبارة افتراضية مع حالة من الذعر ، تفشل التعليمات البرمجية في وقت التشغيل بدلاً من وقت التجميع (الاختبارات لا تساعد لأنهم يعرفون فقط العقد التي كانت موجودة عندما كتبت الاختبارات)
  2. تقوم الكود الخاص بك بفحص مجموعة فرعية من أنواع العقد فقط:
    2.1. هذا النوع الجديد من العقدة لم يكن موجودًا في المجموعة الفرعية على أي حال
    2.1.1. طالما أن هذه العقدة الجديدة لا تحتوي أبدًا على أي من العقد التي تهتم بها ، فإن كل شيء يعمل بشكل جيد
    2.1.2. خلاف ذلك ، فأنت في نفس الموقف كما لو أن التعليمات البرمجية الخاصة بك تتوقع أن تسير في كل عقدة
    2.2. هذا النوع الجديد من العقدة كان من الممكن أن يكون في المجموعة الفرعية التي تهتم بها ، لو علمت عنها.

مع AST المستندة إلى الواجهة تعمل الحالة فقط 2.1.1 بشكل صحيح. هذه صدفة مثلها مثل أي شيء آخر. إصلاح التعليمات البرمجية التدريجي لا يعمل. تسارع AST إلى رفع نسختها وتحتاج التعليمات البرمجية الخاصة بك إلى رفع نسختها.

قد يساعد linter الشامل ولكن نظرًا لأن linter لا يمكنه فحص جميع أنواع الواجهة ، فإنه يجب إخباره بطريقة ما أن واجهة معينة تحتاج إلى التحقق. هذا يعني إما تعليق في المصدر أو نوعًا من ملف التكوين في الريبو الخاص بك. إذا كان تعليقًا في المصدر ، نظرًا لأن تعريف AST يتم تعريفه في مشروع منفصل ، فأنت تحت رحمة هذا المشروع لتمييز الواجهة للتحقق من الشمولية. هذا لا يعمل إلا على نطاق واسع إذا كان هناك نقطة شمولية واحدة يوافق عليها المجتمع بأكمله ويستخدمها دائمًا.

مع AST على أساس المجموع ، ما زلت بحاجة إلى استخدام الإصدار. الاختلاف الوحيد في هذه الحالة هو أن الشمولية linter مضمنة في المترجم.

لا يساعد أي منهما مع 2.2 ، ولكن ماذا يمكن؟

هناك حالة أبسط ، مجاورة لـ AST ، حيث تكون أنواع المجموع مفيدة: الرموز المميزة. لنفترض أنك تكتب معجمًا لآلة حاسبة أبسط. هناك رموز مثل * لا تحتوي على أي قيم مرتبطة بها ورموز مثل Var التي تحتوي على سلسلة تمثل الاسم ، وعلامات مثل Val التي تحتوي على float64 .

يمكنك تنفيذ هذا باستخدام واجهات ولكنه سيكون متعبًا. ربما تفعل شيئًا كهذا ، على الرغم من:

package token
type Type int
const (
  Times Type = iota
  // ...
  Var
  Val
)
type Value struct {
  Type
  Name string // only valid if Type == Var
  Number float64 // only valid if Type == Val
}

يمكن أن تضمن الشمولية على التعدادات المستندة إلى iota عدم استخدام النوع غير القانوني أبدًا ، لكنها لن تعمل بشكل جيد مع شخص يقوم بتعيين الاسم عند النوع == الأوقات أو استخدام الرقم عند النوع == Var. مع تزايد عدد الرموز المميزة ونوعها ، يزداد الأمر سوءًا. حقًا أفضل ما يمكنك فعله هنا هو إضافة طريقة ، Valid() error ، تتحقق من جميع القيود ومجموعة من الوثائق التي تشرح متى يمكنك فعل أي شيء.

نوع المجموع يشفر بسهولة كل هذه القيود وسيكون التعريف هو كل الوثائق المطلوبة. ستكون إضافة نوع جديد من الرموز بمثابة تغيير جذري ولكن كل ما قلته عن AST لا يزال ساريًا هنا.

أعتقد أن المزيد من الأدوات ضروري. أنا فقط لست مقتنعًا بأنه كافٍ.

تضمين التغريدة

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

لا ، ليس على قدم المساواة. يمكنك إجراء كلا التغييرين في نموذج الإصلاح التدريجي (للواجهات: 1. إضافة طريقة جديدة إلى جميع التطبيقات ، 2. إضافة طريقة إلى الواجهة. بالنسبة للحقول الهيكلية: 1. إزالة جميع استخدامات الحقل ، 2. إزالة الحقل). لا يمكن

لا يتعلق الأمر بما إذا كان تغييرًا جذريًا أم لا ، بل يتعلق بما إذا كان تغييرًا جذريًا يمكن تنسيقه بأقل قدر من الانقطاع.

لكن هذا هو السلوك الصحيح والمطلوب.

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

مع AST المستندة إلى الواجهة تعمل الحالة فقط 2.1.1 بشكل صحيح.

لا ، إنه يعمل أيضًا بشكل صحيح في الحالة 1.2 (الفشل في وقت التشغيل للقواعد غير المعترف بها أمر جيد تمامًا. ربما لا أرغب في الذعر ، ولكن فقط أعيد الخطأ) وأيضًا في الكثير من الحالات 2.1. الباقي هو مشكلة أساسية في ترقية البرامج ؛ إذا قمت بإضافة ميزة جديدة إلى مكتبة ، فسيحتاج مستخدمو lib الخاص بك إلى تغيير التعليمات البرمجية للاستفادة منها. هذا لا يعني أن برنامجك غير صحيح حتى يحدث ذلك.

تسارع AST إلى رفع نسختها وتحتاج التعليمات البرمجية الخاصة بك إلى رفع نسختها.

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

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

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

لماذا ا؟ أود أن أزعم أنه من الجيد أن تقوم شركة Switchlint ™ بتقديم شكوى بشأن أي نوع مفتاح بدون حالة افتراضية ؛ بعد كل شيء ، تتوقع أن تعمل الشفرة مع أي تعريف للواجهة ، لذلك من المحتمل أن يكون عدم وجود رمز للعمل مع تطبيقات غير معروفة مشكلة على أي حال. نعم ، هناك استثناءات لهذه القاعدة ، ولكن يمكن بالفعل تجاهل الاستثناءات يدويًا.

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

يمكنك تنفيذ هذا باستخدام واجهات ولكنه سيكون متعبًا.

يتجاهل هذا الجهد المبذول لمرة واحدة في قضية نادرًا ما تظهر. يبدو لي بخير.

و FWIW ، أنا حاليًا أجادل فقط ضد فكرة التحقق الشاملة لأنواع المجموع. ليس لدي أي آراء قوية حتى الآن حول الميزة الإضافية لقول "أي من هذه الأنواع المحددة هيكليًا".

@ Merovius سأضطر إلى التفكير أكثر في نقاطك الممتازة حول الإصلاح التدريجي للكود. في هذه الأثناء:

فحوصات الشمولية

أنا حاليًا أجادل فقط ضد فكرة التحقق الشاملة لأنواع المجموع.

يمكنك بشكل صريح إلغاء الاشتراك في عمليات التحقق من الشمولية باستخدام حالة افتراضية (حسنًا ، بشكل فعال: الوضع الافتراضي يجعله شاملاً عن طريق إضافة حالة تغطي "أي شيء آخر ، مهما كان ذلك"). لا يزال لديك خيار ، ولكن يجب عليك تحديده بشكل صريح.

أود أن أزعم أنه من الجيد أن تقوم شركة Switchlint ™ بتقديم شكوى بشأن أي نوع مفتاح بدون حالة افتراضية ؛ بعد كل شيء ، تتوقع أن تعمل الشفرة مع أي تعريف للواجهة ، لذلك من المحتمل أن يكون عدم وجود رمز للعمل مع تطبيقات غير معروفة مشكلة على أي حال. نعم ، هناك استثناءات لهذه القاعدة ، ولكن يمكن بالفعل تجاهل الاستثناءات يدويًا.

هذه فكرة مثيرة للاهتمام. على الرغم من أنه قد يصل إلى أنواع مجموع تمت محاكاتها بواجهة وتعدادات تمت محاكاتها باستخدام const / iota ، إلا أنه لا يخبرك أنك فاتتك حالة معروفة ، فقط أنك لم تتعامل مع الحالة غير المعروفة. بغض النظر ، يبدو صاخبًا. انصح:

switch {
case n < 0:
case n == 0:
case n > 0:
}

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

switch {
case p[0](a, b):
case p[1](a, b):
//...
case p[N](a, b):
}

حتى إذا كان p[i] يشكل علاقة تكافؤ على أنواع a و b فلن يكون قادرًا على إثبات ذلك ، لذلك يجب عليه وضع علامة على المفتاح على أنه مفقود افتراضي case ، وهو ما يعني طريقة لإسكاته ببيان ، أو تعليق توضيحي في المصدر ، أو نص برمجي مُغلَّف إلى egrep -v خارج القائمة البيضاء ، أو افتراضيًا غير ضروري على المحول الذي يشير خطأً إلى أن p[i] ليست شاملة.

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

الرموز

تطبيقات الرمز البديل:

//Type defined as before
type SimpleToken { Type }
type StringToken { Type; Value string }
type NumberToken { Type; Value float64 }
type Interface interface {
  //some method common to all these types, maybe just token() Interface
}

يؤدي ذلك إلى التخلص من إمكانية تعريف حالة الرمز غير القانوني حيث يكون لشيء ما سلسلة وقيمة رقم ولكن لا يسمح بإنشاء StringToken بنوع يجب أن يكون SimpleToken أو نائب بالعكس.

للقيام بذلك باستخدام الواجهات ، تحتاج إلى تحديد نوع واحد لكل رمز ( type Plus struct{} ، type Mul struct{} ، إلخ.) ومعظم التعريفات هي نفسها تمامًا لاسم النوع. جهد لمرة واحدة أم لا ، هذا كثير من العمل (على الرغم من أنه مناسب تمامًا لإنشاء الكود في هذه الحالة).

أفترض أنه يمكن أن يكون لديك "تسلسل هرمي" لواجهات الرمز المميز لتقسيم أنواع الرموز بناءً على القيم المسموح بها: (بافتراض وجود أكثر من نوع واحد من الرموز المميزة التي يمكن أن تحتوي على رقم أو سلسلة ، وما إلى ذلك في هذا المثال)

type SimpleToken int //implements token.Interface
const (
  Plus SimpleToken = iota
  // ...
}
type NumericToken interface {
  Interface
  Value() float64
  nt() NumericToken
}
type IntToken struct { //implements NumericToken, and a FloatToken
type StringToken interface { // for Var and Func and Const, etc.
  Interface
  Value() string
  st() StringToken
}

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

يمكنك بشكل صريح إلغاء الاشتراك في عمليات التحقق من الشمولية باستخدام حالة افتراضية (حسنًا ، بشكل فعال: الوضع الافتراضي يجعله شاملاً عن طريق إضافة حالة تغطي "أي شيء آخر ، مهما كان ذلك"). لا يزال لديك خيار ، ولكن يجب عليك تحديده بشكل صريح.

لذلك ، يبدو أنه في كلتا الحالتين ، سيكون لدى كل منا خيار الاشتراك أو إلغاء التحقق الشامل :)

لا يخبرك أنك فاتتك قضية معروفة ، فقط أنك لم تتعامل مع الحالة المجهولة.

على نحو فعال ، أعتقد أن المترجم يقوم بالفعل بتحليل البرنامج بالكامل لتحديد الأنواع الملموسة المستخدمة في أي واجهات أعتقد ؟ أتوقع على الأقل أنه ، على الأقل للتأكيدات من نوع غير واجهة (أي تأكيدات النوع التي لا تؤكد على نوع واجهة ، ولكن لنوع ملموس) ، تنشئ جداول الوظائف المستخدمة في الواجهات في وقت الترجمة.
لكن ، بصراحة ، هذا مذكور من المبادئ الأولى ، ليس لدي أي فكرة عن التنفيذ الفعلي.

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

لست مقتنعًا بنسبة 100٪ بإمكانية كتابة أداة يمكن الاعتماد عليها تمامًا مثل توضيح الخيارات بشكل صريح ، لكنني مقتنع بأنه يمكنك تغطية 90٪ من الحالات ويمكنك بالتأكيد كتابة أداة تقوم بذلك خارج نطاق المترجم ، بالنظر إلى التعليقات التوضيحية الصحيحة (على سبيل المثال ، اجعل أنواع المجموع تعليقًا شبيهًا بالعملية ، وليس نوعًا فعليًا). ليس حلا رائعا ، باعتراف الجميع.

بغض النظر ، يبدو صاخبًا. انصح:

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

تطبيقات الرمز البديل:

لماذا لا تكون طريقة التحديد؟ لا تحتاج إلى حقل نوع ، يمكنك الحصول عليه مجانًا من تمثيل الواجهة. إذا كنت قلقًا بشأن تكرار طريقة التحديد مرارًا وتكرارًا ؛ تحديد بنية غير مُصدرة {} ، ومنحها طريقة العلامة هذه وتضمينها في كل تطبيق ، بدون تكلفة إضافية وكتابة أقل لكل خيار من طريقتك.

بغض النظر ، فهذا يعني أن كل رمز يتطلب إحترامًا للمؤشر للوصول إلى قيمته

نعم فعلا. إنها تكلفة حقيقية ، لكنني لا أعتقد أنها تفوق أي حجة أخرى.

أعتقد أن هذا غير عادل.

هذا صحيح.

لقد كتبت إصدارًا سريعًا وقذرًا وقمت بتشغيله على stdlib. التحقق من أي بيان تبديل يحتوي على 1956 نتيجة ، مما أدى إلى تقييده لتخطي النموذج switch { أدى إلى تقليل هذا العدد إلى 1677. لم أقم بفحص أي من هذه المواقع لمعرفة ما إذا كانت النتيجة ذات مغزى.

https://github.com/jimmyfrasche/switchlint

هناك بالتأكيد مجال كبير للتحسين. إنه ليس معقدًا بشكل رهيب. نرحب بطلبات السحب.

(سأرد على الباقي لاحقًا)

تحرير: تنسيق ترميز خاطئ

أعتقد أن هذا ملخص (متحيز تمامًا) لكل شيء حتى الآن (وافتراضًا نرجسيًا اقتراحي الثاني)

الايجابيات

  • موجز ، من السهل كتابة عدد من القيود بإيجاز بطريقة التوثيق الذاتي
  • تحكم أفضل في التخصيصات
  • أسهل في التحسين (كل الاحتمالات المعروفة للمترجم)
  • فحص شامل (عند الرغبة ، يمكن الانسحاب)

سلبيات

  • أي تغيير على أعضاء نوع المجموع هو تغيير فاصل ، ولا يسمح بإصلاح الكود التدريجي ما لم تنسحب جميع الحزم الخارجية من فحوصات الشمولية
  • شيء آخر في اللغة يجب تعلمه ، يتداخل بعض المفاهيم مع الميزات الموجودة
  • جامع القمامة يجب أن يعرف الأعضاء الذين هم مؤشرات
  • محرجًا للمبالغ على شكل 1 + 1 + ⋯ + 1

البدائل

  • iota "enum" للمجاميع بالشكل 1 + 1 + ⋯ + 1
  • واجهات مع طريقة علامة غير مُصدرة لمجموعات أكثر تعقيدًا (ربما تم إنشاؤها)
  • أو بنية باستخدام تعداد iota وقواعد لغوية إضافية حول الحقول التي يتم تعيينها بناءً على قيمة التعداد

بغض النظر

  • أدوات أفضل ، أدوات أفضل دائمًا

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

قد يكون هناك إعلان من مشرف الحزمة "مرحبًا ، سنضيف عضوًا جديدًا إلى نوع المجموع هذا في الإصدار التالي ، تأكد من أنه يمكنك التعامل معه" ومن ثم يمكن لأداة switchlint العثور على أي حالات تحتاج إلى يتم استبعادها.

ليس واضحًا مثل الحالات الأخرى ، ولكن لا يزال ممكنًا تمامًا.

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

لنفترض أننا نرغب في الاشتراك في التحقق من الشمولية باستخدام linter عند استخدام أنواع الواجهة التي تحاكي أنواع المجموع ، بغض النظر عن الحزمة التي تم تحديدها فيها.

Merovius خدعتك betterSumType() BetterSumType رائعة جدًا ، لكن هذا يعني أن التبديل يجب أن يحدث في الحزمة المحددة (أو تعرض شيئًا مثل

func CallBeforeSwitches(b BetterSumType) (BetterSumType, bool) {
    if b == nil {
        return nil, false
    }
    b = b.betterSumType()
    if b == nil {
        return nil, false
    }
    return b, true
}

وأيضًا يتم استدعاء ذلك في كل مرة).

ما هي المعايير اللازمة للتحقق من أن جميع المفاتيح في البرنامج شاملة؟

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

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

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

لكي نكون دقيقين ، نحتاج إما إلى التحقق من أن القيمة الصفرية للواجهة لا يتم تمريرها مطلقًا أو أن نفرض أن مفتاحًا شاملاً يتحقق من case nil أيضًا. (الأخير أسهل ولكن الأول مفضل لأن تضمين لا شيء يحول مجموع "النوع A أو النوع B أو النوع C" إلى مجموع "لا شيء أو النوع A أو النوع B أو النوع C").

لنفترض أن لدينا لينتير ، مع كل هذه القدرات ، حتى الاختيارية منها ، يمكنه التحقق من هذه الدلالات لأي شجرة من الواردات وأي واجهة معينة داخل تلك الشجرة.

لنفترض الآن أن لدينا مشروعًا بتبعية D. نريد التأكد من أن الواجهة المحددة في إحدى حزم D شاملة في مشروعنا. دعنا نقول إنه كذلك.

الآن ، نحتاج إلى إضافة تبعية جديدة لمشروعنا D ′. إذا استوردت D ′ الحزمة في D التي حددت نوع الواجهة المعنية ولكنها لا تستخدم هذا اللينت ، فيمكنها بسهولة تدمير الثوابت التي تحتاج إلى الاحتفاظ بها لاستخدام مفاتيح شاملة.

بالنسبة لهذه المسألة ، دعنا نقول أن D قد تجاوز للتو linter بالصدفة ليس لأن المشرف يديره. يمكن للترقية إلى D أن تدمر العناصر الثابتة بسهولة مثل D ′.

حتى لو كان بإمكان linter أن يقول "في الوقت الحالي هذا شامل بنسبة 100٪" يمكن أن يتغير دون أن نفعل أي شيء.

مدقق شمولية "تعداد ذرة" يبدو أسهل.

لجميع type t u حيث u مكمل ويستخدم t كقيمة const إما بقيم محددة بشكل فردي أو iota مثل الصفر قيمة u ضمن هذه الثوابت.

ملحوظات:

  • يمكن التعامل مع القيم المكررة كأسماء مستعارة ويتم تجاهلها في هذا التحليل. سنفترض أن جميع الثوابت المسماة لها قيم مميزة.
  • قد يتم التعامل مع 1 << iota كمجموعة طاقة ، أعتقد على الأقل معظم الوقت ، ولكن من المحتمل أن يتطلب شروطًا إضافية ، لا سيما مكمل أحادي البتات المحيط. في الوقت الحاضر ، لن يتم النظر فيها

بالنسبة إلى بعض الاختصارات ، دعنا نسمي min(t) الثابت مثل أي ثابت آخر ، C ، min(t) <= C ، وبالمثل ، دعنا نسمي max(t) بالثابت مثل هذا لأي ثابت آخر ، C ، C <= max(t) .

لضمان استخدام t بشكل شامل ، نحتاج إلى التأكد من ذلك

  • قيم t هي دائمًا الثوابت المسماة (أو 0 في مواضع اصطلاحية معينة ، مثل استدعاء الوظيفة)
  • لا توجد مقارنات عدم مساواة بقيمة t ، v ، خارج min(t) <= v <= max(t)
  • قيم t لا تُستخدم أبدًا في العمليات الحسابية + ، / ، إلخ. قد يكون الاستثناء المحتمل عندما يتم تثبيت النتيجة بين min(t) و max(t) بعد ذلك مباشرة ، ولكن قد يكون من الصعب اكتشاف ذلك بشكل عام ، لذلك قد يتطلب الأمر تعليقًا توضيحيًا في التعليقات وربما يجب أن يقتصر على الحزمة التي تحدد t .
  • تحتوي المفاتيح على جميع ثوابت t أو حالة افتراضية.

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

ما أفهمه هو أن هذا ، على غرار الأسماء المستعارة من النوع ، لن يؤدي إلى كسر التغييرات ، فلماذا نؤجله في Go 2؟

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

يعمل الاسم المستعار للنوع على إصلاح مشكلة لم يكن هناك حل بديل لها. توفر أنواع المجموع فائدة في أمان النوع ، ولكنها ليست مانعًا لعدم امتلاكها.

مجرد نقطة واحدة (ثانوية) لصالح شيء مثل اقتراحrogpeppe الأصلي. في الحزمة http ، هناك نوع الواجهة Handler ونوع الوظيفة التي تنفذها ، HandlerFunc . في الوقت الحالي ، لتمرير دالة إلى http.Handle ، يجب عليك صراحة تحويلها إلى HandlerFunc . إذا قبل http.Handle بدلاً من ذلك وسيطة من النوع HandlerFunc | Handler ، فقد يقبل أي دالة / إغلاق يمكن تخصيصه لـ HandlerFunc مباشرةً. يعمل الاتحاد بشكل فعال كتلميح نوع يخبر المترجم كيف يمكن تحويل القيم ذات الأنواع غير المسماة إلى نوع الواجهة. نظرًا لأن HandlerFunc ينفذ Handler ، سيتصرف نوع الاتحاد تمامًا مثل Handler وإلا.

griesemer ردًا على تعليقك في سلسلة التعداد ، https://github.com/golang/go/issues/19814#issuecomment -322752526 ، أعتقد أن اقتراحي سابقًا في هذا الموضوع https://github.com/golang/ go / issue / 19412 # issuecomment -289588569 يتناول السؤال المتعلق بكيفية عمل أنواع المجموع ("تعدادات النمط السريع") في Go. بقدر ما أريدهم ، لا أعرف ما إذا كانوا سيكونون إضافة ضرورية إلى Go ، لكنني أعتقد أنه إذا تمت إضافتهم ، فسيتعين عليهم البحث / التشغيل كثيرًا من هذا القبيل.

هذا المنشور غير مكتمل وهناك توضيحات خلال هذا الموضوع ، قبله وبعده ، لكنني لا أمانع في تكرار هذه النقاط أو تلخيصها لأن هذا الموضوع طويل جدًا.

إذا كان لديك نوع مجموع متزامن مع واجهة بعلامة نوع ولا يمكنك مطلقًا التحايل عليه من خلال التضمين ، فهذا هو أفضل دفاع توصلت إليه: https://play.golang.org/p/FqdKfFojp-

jimmyfrasche لقد كتبت هذا منذ فترة.

نهج آخر محتمل هو هذا: https://play.golang.org/p/p2tFm984S8

rogpeppe إذا كنت ستستخدم التفكير فلماذا لا تستخدم الانعكاس فقط؟

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

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

تحرير: لقد أزلت القدرة على كتابة تأكيد على القيمة الديناميكية لقيمة الاختيار. إنه أمر سحري للغاية ويتم تقديم سبب السماح به أيضًا من خلال إنشاء الكود.

Edit2: أوضح كيفية عمل أسماء الحقول مع التأكيدات والمفاتيح عند تحديد الانتقاء في حزمة أخرى.

Edit3: التضمين المقيد وأسماء الحقول الضمنية الموضحة

Edit4: توضيح الافتراضي في التبديل

اختر الأنواع

الانتقاء هو نوع مركب من الناحية التركيبية مشابه للبنية:

pick {
  A, B S
  C, D T
  E U "a pick tag"
}

في ما سبق ، أسماء الحقول المختارة هي A و B و C و D و E و S و T و U هي الأنواع المناسبة لهذه الحقول. قد يتم تصدير أسماء الحقول أو عدم تصديرها.

قد لا يكون الانتقاء تكراريًا بدون مراوغة.

قانوني

type p pick {
    //...
    p *p
}

غير قانوني

type p pick {
    //...
    p p
}

لا يوجد تضمين للاختيارات ، ولكن قد يتم تضمين اختيار في بنية. إذا تم تضمين اختيار في بنية ، فسيتم ترقية الطريقة الموجودة على الانتقاء إلى البنية ولكن حقول الاختيار ليست كذلك.

النوع الذي لا يحتوي على اسم حقل هو اختصار لتعريف حقل بنفس اسم النوع. (هذا خطأ إذا كان النوع غير مسمى ، باستثناء *T حيث يكون الاسم T ).

على سبيل المثال،

type p pick {
    io.Reader
    io.Writer
    string
}

يحتوي على ثلاثة حقول Reader ، Writer ، و string ، مع الأنواع المعنية. لاحظ أن الحقل string غير مُصدّر بالرغم من وجوده في نطاق الكون.

تتكون قيمة نوع الانتقاء من حقل ديناميكي وقيمة هذا الحقل.

القيمة الصفرية لنوع الانتقاء هي حقله الأول في ترتيب المصدر والقيمة الصفرية لهذا الحقل.

بالنظر إلى قيمتين من نفس نوع الاختيار ، a و b ، يمكن تعيين قيمة الانتقاء كأي قيمة أخرى

a = b

يعد تعيين قيمة غير قابلة للانتقاء ، حتى لو كان نوعًا من أحد الحقول في الاختيار ، أمرًا غير قانوني.

نوع الانتقاء يحتوي على حقل ديناميكي واحد فقط في أي وقت.

يشبه التركيب الحرفي المركب البنيات ، لكن هناك قيودًا إضافية. وبالتحديد ، تكون القيم الحرفية بدون مفتاح غير صالحة دائمًا ويمكن تحديد مفتاح واحد فقط.

ما يلي صالح

pick{A string; B int}{A: "string"} //value is (B, "string")
pick{A, B int}{B: 1} //value is (B, 1)
pick{A, B string}{} //value is (A, "")

فيما يلي أخطاء وقت الترجمة:

pick{A int; B string}{A: 1, B: "string"} //a pick can only have one value at a time
pick{A int; B uint}{1} //pick composite literals must be keyed

إعطاء قيمة p من النوع pick {A int; B string} التعيين التالي

p.B = "hi"

يعين الحقل الديناميكي p إلى B وقيمة B إلى "hi".

يؤدي التعيين إلى الحقل الديناميكي الحالي إلى تحديث قيمة هذا الحقل. يجب أن يكون التعيين الذي يعيّن حقلاً ديناميكيًا جديدًا صفراً لأي مواقع ذاكرة غير محددة. يتم تحديث التعيين إلى حقل الانتقاء أو البنية لحقل الانتقاء أو تعيين الحقل الديناميكي حسب الضرورة.

type P pick {
    A, B image.Point
}

var p P
fmt.Println(P) //{A: {0 0}}

p.A.X = 1 //A is the dynamic field, update
fmt.Println(P) //{A: {1 0}}

p.B.Y = 2 //B is not the dynamic value, create zero image.Point first
fmt.Println(P) //{B: {0 2}}

لا يمكن الوصول إلى القيمة المحتفظ بها في الانتقاء إلا عن طريق تأكيد الحقل أو تبديل الحقل.

x := p.[X] //panics if X is not the dynamic field of p
x, ok := p.[X] //returns the zero value of X and false if X is not the dynamic field of p

switch v := p.[var] {
case A:
case B, C: // v is only defined in this case if fields B and C have identical type names
case D:
default: // always legal even if all fields are exhaustively listed above
}

أسماء الحقول في تأكيدات الحقول ومفاتيح تبديل الحقول هي خاصية من النوع ، وليست الحزمة التي تم تعريفها فيها. فهي ليست مؤهلة ولا يمكن أن تكون مؤهلة بواسطة اسم الحزمة الذي يحدد pick .

هذا صحيح:

_, ok := externalPackage.ReturnsPick().[Field]

هذا غير صالح:

_, ok := externalPackage.ReturnsPick().[externalPackage.Field]

دائمًا ما تُرجع تأكيدات الحقول ومفاتيح التبديل الميدانية نسخة من قيمة الحقل الديناميكي.

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

تعمل تأكيدات الكتابة ومفاتيح الكتابة أيضًا على اللقطات.

//removed, see note at top
//v, ok := p.(fmt.Stringer) //holds if the type of the dynamic field implements fmt.Stringer
//v, ok := p.(int) //holds if the type of the dynamic field is an int

ترجع تأكيدات الكتابة ومفاتيح الكتابة دائمًا نسخة من قيمة الحقل الديناميكي.

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

إذا كانت جميع أنواع الانتقاء تدعم عوامل تشغيل المساواة ، فحينئذٍ:

  • يمكن استخدام قيم هذا الاختيار كمفاتيح خريطة
  • قيمتان لنفس الاختيار هما == إذا كان لديهم نفس الحقل الديناميكي وقيمته هي ==
  • قيمتان بحقول ديناميكية مختلفة هما != حتى لو كانت القيم == .

لا يتم دعم أي عوامل تشغيل أخرى على قيم من نوع الانتقاء.

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

إذا تم تعريف P و Q في حزم مختلفة وتحتوي على حقول غير مُصدرة ، فإن هذه الحقول تعتبر مختلفة بغض النظر عن الاسم والنوع.

مثال:

type P pick {A int; B string}
type Q pick {B string; A int; C float64}

//legal
var p P
q := Q(p)

//illegal
var q Q
p := P(Q) //cannot handle field C

يتم تعريف التخصيص بين نوعين من أنواع الانتقاء على أنه قابلية التحويل ، طالما لم يتم تحديد أكثر من نوع واحد.

قد يتم التصريح عن الأساليب على نوع انتقاء محدد.

أنشأت (وأضفت إلى الويكي) تقرير تجربة https://gist.github.com/jimmyfrasche/ba2b709cdc390585ba8c43c989797325

تحرير: و: القلب: إلى mewmew الذي ترك تقريرًا أفضل بكثير وأكثر تفصيلاً كرد على هذا الجوهر

ماذا لو كانت لدينا طريقة لنقول ، بالنسبة لنوع معين T ، قائمة الأنواع التي يمكن تحويلها إلى النوع T أو تعيينها لمتغير من النوع T ؟ على سبيل المثال

type T interface{} restrict { string, error }

يعرّف نوع واجهة فارغ باسم T بحيث تكون الأنواع الوحيدة التي يمكن تخصيصها لها هي string أو error . أي محاولة لتعيين قيمة من أي نوع آخر ينتج خطأ في وقت الترجمة. الآن أستطيع أن أقول

func FindOrFail(m map[int]string, key int) T {
    if v, ok := m[key]; ok {
        return v
    }
    return errors.New("no such key")
}

func Lookup() {
    v := FindOrFail(m, key)
    if err, ok := v.(error); ok {
        log.Fatal(err)
    }
    s := v.(string) // This type assertion must succeed.
}

ما هي العناصر الرئيسية لأنواع المجموع (أو أنواع الاختيار) التي لن ترضي بهذا النوع من النهج؟

s := v.(string) // This type assertion must succeed.

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

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

تضمين التغريدة

الايجابيات

  • من الممكن حصره في مجموعة مغلقة من الأنواع - وهذا بالتأكيد هو الشيء الرئيسي
  • من الممكن كتابة مدقق شامل دقيق
  • يمكنك الحصول على "يمكنك تعيين قيمة ترضي العقد لهذه" الممتلكات. (أنا لا أهتم بهذا ، لكني أتخيل أن الآخرين يفعلون ذلك).

السلبيات

  • إنها مجرد واجهات مع الفوائد وليست نوعًا مختلفًا من النوع (فوائد جيدة رغم ذلك!)
  • لا يزال لديك لا شيء ، لذا فهو ليس نوع مجموع بالمعنى النظري للنوع. مهما كان A + B + C الذي تحدده فهو في الحقيقة 1 + A + B + C ليس لديك خيار بشأنه. كما أشار stevenblenkinsop أثناء
  • والأهم من ذلك ، بسبب هذا المؤشر الضمني ، لديك دائمًا مراوغة. مع اقتراح الانتقاء ، يمكنك اختيار الحصول على p أو *p مما يمنحك مزيدًا من التحكم في مقايضات الذاكرة. لا يمكنك تنفيذها كنقابات تمييزية (بمعنى C) كتحسين.
  • لا يوجد خيار للقيمة الصفرية ، وهي خاصية رائعة حقًا خاصةً أنه من المهم جدًا في Go الحصول على قيمة صفرية مفيدة قدر الإمكان
  • من المفترض أنك لا تستطيع تحديد طرق على T (ولكن من المفترض أن يكون لديك طرق للواجهة التي يعدلها التقييد ولكن الأنواع الموجودة في التقييد يجب أن تفي بها؟ وإلا فإنني لا أرى الهدف من ليس مجرد الحصول على type T restrict {string, error} )
  • إذا فقدت تسميات الحقول / الملخصات / ما الذي لديك ، فسيصبح الأمر محيرًا عندما تتفاعل مع أنواع الواجهة. تفقد خاصية "هذا بالضبط أو تلك" القوية لأنواع المجموع. يمكنك وضع io.Reader وسحب io.Writer للخارج. هذا منطقي بالنسبة للواجهات (غير المقيدة) ولكن ليس أنواع المجموع.
  • إذا كنت تريد أن يكون لديك نوعان متطابقان يعنيان أشياء مختلفة ، فأنت بحاجة إلى استخدام أنواع الغلاف لإزالة الغموض ؛ يجب أن تكون مثل هذه العلامة في مساحة اسم خارجية بدلاً من أن تقتصر على نوع بالطريقة التي يكون بها حقل الهيكل
  • قد يكون هذا بمثابة قراءة أكثر من اللازم في صياغتك المحددة ، ولكن يبدو أنه يغير قواعد التخصيص بناءً على نوع المحال إليه (أقرأه كقول إنه لا يمكنك تعيين شيء قابل للتخصيص إلى error إلى T يجب أن يكون خطأ بالضبط).

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

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

تضمين التغريدة
في اقتراحك المحدث ، هل ستكون المهمة التالية ممكنة ، إذا كانت جميع عناصر النوع من أنواع مميزة:

type p pick {
    A int
    B string
}

func Foo(P p) {
}

var P p = 42
var Q p = "foo"

Foo(42)
Foo("foo")

إن قابلية استخدام أنواع المجموع عندما تكون مثل هذه التخصيصات ممكنة أكبر بكثير.

مع اقتراح الانتقاء ، يمكنك اختيار الحصول على p أو *p مما يمنحك مزيدًا من التحكم في مقايضات الذاكرة.

السبب في تخصيص الواجهات لتخزين القيم العددية هو أنك لست مضطرًا لقراءة كلمة كتابة لتقرر ما إذا كانت الكلمة الأخرى هي مؤشر ؛ انظر # 8405 للمناقشة. من المحتمل أن تنطبق نفس اعتبارات التنفيذ على نوع الانتقاء ، مما قد يعني عمليًا أن p ينتهي به الأمر بالتخصيص وأن يكون غير محلي على أي حال.

urandom لا ، نظرًا لتعريفاتك ، يجب كتابتها

var p P = P{A: 42} // p := P{A: 42}
var q P = P{B: "foo")
Foo(P{A: 42}) // or Foo({A: 42}) if types can be elided here
Foo(P{B: "foo"})

من الأفضل التفكير فيهم على أنهم هيكل لا يمكن أن يكون له سوى حقل واحد في كل مرة.

إذا لم يكن لديك ذلك ثم أضفت C uint إلى p ماذا يحدث لـ p = 42 ؟

يمكنك تكوين الكثير من القواعد بناءً على الترتيب وإمكانية التخصيص ولكنها تعني دائمًا أن التغييرات في تعريف النوع يمكن أن يكون لها تأثيرات دقيقة ودراماتيكية على جميع التعليمات البرمجية التي تستخدم النوع.

في أفضل الأحوال ، يؤدي التغيير إلى كسر كل الكود بالاعتماد على عدم الغموض ويقول إنك بحاجة إلى تغييره إلى p = int(42) أو p = uint(42) قبل أن يتم تجميعه مرة أخرى. يجب ألا يتطلب تغيير سطر واحد إصلاح مائة سطر. خاصة إذا كانت هذه السطور في حزم من الأشخاص اعتمادًا على الكود الخاص بك.

عليك إما أن تكون صريحًا بنسبة 100٪ أو أن يكون لديك نوع هش للغاية لا يمكن لأحد لمسه لأنه قد يكسر كل شيء.

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

josharian ، لذا إذا قرأت ذلك بشكل صحيح ، فإن سبب iface هو دائمًا (*type, *value) بدلاً من إخفاء قيم حجم الكلمات في الحقل الثاني كما فعل Go سابقًا بحيث لا يحتاج GC المتزامن إلى فحص كليهما لمعرفة ما إذا كان الثاني مؤشرًا أم لا - يمكنه فقط افتراض أنه دائمًا كذلك. هل فهمت ذلك بشكل صحيح؟

بمعنى آخر ، إذا تم تنفيذ نوع الانتقاء (باستخدام تدوين C) مثل

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

سيحتاج GC إلى أخذ قفل (أو شيء خيالي ولكن ما يعادله) لفحص which لتحديد ما إذا كان يلزم فحص summands ؟

سبب iface الآن دائمًا (* type ، * value) بدلاً من إخفاء قيم حجم الكلمة في الحقل الثاني كما فعل Go سابقًا هو أن GC المتزامن لا يحتاج إلى فحص كلا الحقلين لمعرفة ما إذا كان الثاني هو مؤشر —يمكننا أن نفترض أنه دائمًا كذلك.

هذا صحيح.

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

pick {
  a uintptr
  b string
  c []byte
}

يمكن وضعها تقريبًا:

[ word 1 (ptr) ] [ word 2 (non-ptr) ] [ word 3 (non-ptr) ]
[    <nil>         ] [                 a           ] [                              ]
[       b.ptr      ] [            b.len          ] [                              ]
[       c.ptr      ] [             c.len         ] [        c.cap             ]

لكن أنواع الانتقاء الأخرى قد لا تسمح بهذا التغليف الأمثل. (آسف على ASCII المكسور ، لا يمكنني أن أجعل GitHub يعرضه بشكل صحيح. آمل أن تحصل على هذه النقطة.)

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

josharian وشكرا لك على القيام بذلك. لم أفكر في ذلك (بكل صراحة ، لقد بحثت في google إذا كان هناك بحث حول كيفية تمييز النقابات التي يمارسها GC ، ورأيت أنه يمكنك فعل ذلك وأطلق عليه اسم اليوم - لسبب ما لم يربط عقلي بـ "التزامن" مع "Go" في ذلك اليوم: راحة اليد!).

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

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

سيكون من العار أن تفقد خصائص الحجم الجميل ولكن هذا في الحقيقة مجرد تحسين.

حتى لو كانت دائمًا 1 + حجم بنية مكافئة حتى عندما لا تحتوي على مؤشرات ، فستظل تحتوي على جميع الخصائص الرائعة الأخرى للنوع نفسه ، بما في ذلك التحكم في التخصيصات. يمكن إضافة تحسينات إضافية بمرور الوقت وستكون ممكنة على الأقل كما أشرت.

type p pick {
    A int
    B string
}

هل يجب أن يكون هناك "أ" و "ب"؟ يتم اختيار مجموعة من الأنواع ، فلماذا لا تتخلص من أسماء المعرفات الخاصة بهم تمامًا:

type p pick {
    int
    string
}
q := p{string: "hello"}

أعتقد أن هذا النموذج صالح بالفعل للبنية. يمكن أن يكون هناك قيد مطلوب للاختيار.

@ كما لو تم حذف اسم الحقل ، فسيكون هو نفس النوع بحيث يعمل المثال الخاص بك ، ولكن نظرًا لأن أسماء الحقول هذه غير مُصدرة ، فلا يمكن تعيينها / الوصول إليها إلا من داخل الحزمة التعريفية.

يجب أن تكون أسماء الحقول موجودة ، حتى لو تم إنشاؤها بشكل ضمني بناءً على اسم النوع ، أو أن هناك تفاعلات سيئة مع إمكانية التخصيص وأنواع الواجهة. أسماء الحقول هي ما يجعلها تعمل مع بقية Go.

عذرًا ، أدركت للتو أنك تعني شيئًا مختلفًا عما قرأته.

تعمل صيغتك ولكن بعد ذلك لديك أشياء تشبه حقول البنية ولكنها تتصرف بشكل مختلف بسبب الشيء المعتاد المُصدَّر / غير المُصدَّر.

هل يمكن الوصول إلى سلسلة من خارج الحزمة التي تحدد p لأنها موجودة في الكون؟

ماذا عن

type t struct {}
type P pick {
  t
  //other stuff
}

؟

من خلال فصل اسم الحقل عن اسم النوع ، يمكنك القيام بأشياء مثل

pick {
  unexported Exported
  Exported unexported
}

او حتى

pick { Recoverable, Fatal error }

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

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

package p
type T struct{
    Exported t
}
type t struct{}

يوجد _t_ الصغير في حزمة حيث يتم تضمينه في T كبير ، ويتم التعرض فقط من خلال هذه الأنواع المصدرة.

@كما

لست متأكدًا من أنني أتابع تمامًا:

//with the option to have field names
pick { //T is in the namespace of the pick and the type isn't exposed to other packages
  T t
  //...
}

//without
type T = t //T has to be defined in the outer scope and now t is exposed to other packages
pick {
  T
  //...
}

أيضًا ، إذا كان لديك اسم النوع فقط للتسمية ، لتضمين []string على سبيل المثال ، فستحتاج إلى القيام بـ type Strings = []string .

هذا هو إلى حد كبير الطريقة التي أريد أن أرى بها اختيار الأنواع المنفذة. في
على وجه الخصوص ، إنها الطريقة التي يعمل بها Rust و C ++ (المعايير الذهبية للأداء)
هو - هي.

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

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

في 18 آب (أغسطس) 2017 ، الساعة 12:01 ظهرًا ، كتب "jimmyfrasche" [email protected] :

josharian https://github.com/josharian لذا إذا قرأت ذلك بشكل صحيح
سبب iface هو دائمًا (* type ، * value) بدلاً من التخزين المؤقت
قيم حجم الكلمة في الحقل الثاني كما فعل Go سابقًا هي أن ملف
لا يحتاج GC المتزامن إلى فحص كلا الحقلين لمعرفة ما إذا كان الثاني
هو مؤشر — يمكنه فقط افتراض أنه كذلك دائمًا. هل فهمت ذلك بشكل صحيح؟

بمعنى آخر ، إذا تم تنفيذ نوع الانتقاء (باستخدام تدوين C) مثل

هيكل {
الباحث الذي
اتحاد {
أ أ ؛
ب ب ؛
نسخة؛
} موجزات ؛
}

سيحتاج GC إلى قفل (أو شيء خيالي ولكنه مكافئ) لـ
فحص أيهما لتحديد ما إذا كان يلزم مسح الاستمارات ضوئيًا؟

-
أنت تتلقى هذا لأنك قمت بتأليف الموضوع.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/19412#issuecomment-323393003 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AGGWB3Ayi31dYwotewcfgmCQL-XVrfxIks5sZbVrgaJpZM4MTmSr
.

تضمين التغريدة

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

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

كمثال على سبب صحة ذلك ، ضع في اعتبارك للأجيال القادمة

v := pick{ A int; B bool }{A: 5}
p := &v.[A] //(this would be illegal but pretending it's not for a second)
v.B = true

إذا تم تحسين v بحيث تشغل الحقول A و B نفس الموضع في الذاكرة ، فإن p لا يشير إلى int: إنه يشير إلى منطقي. تم انتهاك سلامة الذاكرة.

تضمين التغريدة

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

v := pick{ A int; ... }{A: 5}
v2 := v

v2.[A] = 6 // (this would be illegal under the proposal, but would be 
           // permitted if `v2.[A]` were addressable)

fmt.Println(v.[A]) // would print 6 if the contents of the pick are stored indirectly

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

تحرير: عفوًا (انظر أدناه)

تضمين التغريدة

القيمة الصفرية لنوع الانتقاء هي حقله الأول في ترتيب المصدر والقيمة الصفرية لهذا الحقل.

لاحظ أن هذا لن ينجح إذا احتاج الحقل الأول إلى تخزينه بشكل غير مباشر ، إلا إذا قمت بحالة خاصة للقيمة الصفرية بحيث يقوم v.[A] و v.(error) بالشيء الصحيح.

stevenblenkinsop لست متأكدًا مما

منح

var p pick { A error; B int }

القيمة الصفرية ، p ، بها حقل ديناميكي A وقيمة A لا شيء.

لم أكن أشير إلى القيمة المخزنة في pick / الذي يحتوي على مؤشر ، كنت أشير إلى قيمة غير مؤشر يتم تخزينها بشكل غير مباشر بسبب قيود التخطيط التي يفرضها جامع القمامة ، كما وصفها @ josharian .

في المثال الخاص بك ، p.B - ليس مؤشرًا - لن يكون قادرًا على مشاركة التخزين المتداخل مع p.A ، والذي يتكون من مؤشرين. من المرجح أن يتم تخزينها بشكل غير مباشر (على سبيل المثال ، يتم تمثيلها على أنها *int يتم إلغاء الإشارة إليها تلقائيًا عند الوصول إليها ، بدلاً من كونها int ). إذا كان p.B هو الحقل الأول ، فستكون القيمة الصفرية لـ pick هي new(int) ، وهي ليست قيمة صفرية مقبولة لأنها تتطلب التهيئة. ستحتاج إلى حالة خاصة بحيث لا يتم التعامل مع أي شيء *int على أنه new(int) .

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

تحرير: يصيح ، حالة السباق. نشر ثم رأى تعليقك.

stevenblenkinsop آه ، حسنًا ، أرى ما

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

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

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

struct {
  which_field int // 0 = A, 1 = B
  A error
  B int
}

لا تزال القيمة الصفرية عبارة عن صفر بايت ولا داعي للتخصيص خلسة كحالة خاصة.

الجزء المهم هو ضمان وجود حقل واحد فقط يلعب في وقت معين.

كان الدافع للسماح بتأكيدات النوع / المفاتيح عند اللقطات ، على سبيل المثال ، إذا كان كل نوع في الاختيار راضٍ fmt.Stringer يمكنك كتابة طريقة على الاختيار مثل

func (p P) String() string {
  return p.(fmt.Stringer).String()
}

ولكن نظرًا لأن أنواع حقول الانتقاء يمكن أن تكون واجهات ، فإن هذا يخلق دقة.

إذا كان اختيار P في المثال السابق يحتوي على حقل نوعه هو نفسه fmt.Stringer String ستفزع إذا كان هذا هو الحقل الديناميكي وقيمته nil . لا يمكنك كتابة تأكيد واجهة nil لأي شيء ، ولا حتى نفسها. https://play.golang.org/p/HMYglwyVbl على الرغم من أن هذا كان صحيحًا دائمًا ، إلا أنه لا يتم عرضه بانتظام ، ولكنه قد يأتي بشكل أكثر انتظامًا مع اللقطات.

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

سيكون من المدهش أيضًا ، إذا كان بإمكانك تنفيذ عمليات عند الاختيار ، أن هذه الطرق لا تُستخدم لتلبية تأكيد النوع.

type Num pick { A int; B float32 }

func (n Num) String() string {
      switch v := n.[var] {
      case A:
          return fmt.Sprint(v)
      case B:
          return fmt.Sprint(v)
      }
}
...
n := Num{A: 5}
s1, ok := p.(fmt.Stringer) // ok == false
var i interface{} = p
s2, ok := i.(fmt.Stringer) // ok == true

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

تحرير: بالمناسبة ، تعبئة اختيار على النحو الأمثل هو مثال لأقصر مشكلة

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

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

رغم ذلك ، سيكون من السهل إلى حد ما استخدام إنشاء الكود لكتابة ملف

func (p Pick) String() string {
  switch v := p.[var] {
  case A:
    return v.String()
  case B:
    return v.String()
  //etc
  }
}

تقدمت للتو وأسقطت تأكيدات الكتابة. ربما يجب إضافتهم لكنهم ليسوا جزءًا ضروريًا من الاقتراح.

أريد أن أعود إلى تعليقianlancetaylor السابق ، لأنني حصلت على منظور جديد حوله بعد التفكير في المزيد حول معالجة الأخطاء (على وجه التحديد ، https://github.com/golang/go/issues/21161# إصدار - 320294933).

على وجه الخصوص ، ما الذي يمنحنا إياه النوع الجديد ولا نحصل عليه من أنواع الواجهات؟

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

لدينا حاليا الكثير من وظائف النموذج

func F(…) (T, error) {
    …
}

بعضها ، مثل io.Reader.Read و io.Reader.Write ، يُرجع T مع error ، بينما يُرجع البعض الآخر إما T أو error لكن ليس كلاهما مطلقًا. بالنسبة للنمط السابق لواجهة برمجة التطبيقات ، غالبًا ما يكون تجاهل T في حالة حدوث خطأ خطأ (على سبيل المثال ، إذا كان الخطأ io.EOF ) ؛ للنمط الأخير ، إرجاع الخطأ T غير الصفر.

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

على سبيل المثال، proto.Marshal يراد لها أن تكون "القيمة والخطأ" أسلوب إذا كان الخطأ هو RequiredNotSetError ، ولكن يبدو أن "قيمة أو خطأ" أسلوب غير ذلك. نظرًا لأن نظام الكتابة لا يميز بين الاثنين ، فمن السهل إدخال الانحدارات عن طريق الخطأ: إما عدم إرجاع قيمة عندما ينبغي لنا ذلك ، أو إرجاع قيمة عندما لا ينبغي لنا ذلك. كما أن تطبيق proto.Marshaler يزيد الأمر تعقيدًا.

من ناحية أخرى ، إذا تمكنا من التعبير عن النوع على أنه اتحاد ، فيمكننا أن نكون أكثر وضوحًا بشأنه:

type PartialMarshal struct {
    Data []byte // The marshalled value, ignoring unset required fields.
    MissingFields []string
}

func Marshal(pb Message) []byte | PartialMarshal | error

ianlancetaylor ، لقد كنت

منح

var r interface{} restrict { uint, int } = 1

النوع الديناميكي لـ r هو int ، و

var _ interface{} restrict { uint32, int32 } = 1

غير قانوني.

منح

type R interface{} restrict { struct { n int }, etc }
type S struct { n int }

ثم var _ R = S{} سيكون غير قانوني.

لكن معطى

type R interface{} restrict { int, error }
type A interface {
  error
  foo()
}
type C struct { error }
func (C) foo() {}

سيكون كلا من var _ R = C{} و var _ R = A(C{}) قانونيًا.

على حد سواء

interface{} restrict { io.Reader, io.Writer }

و

interface{} restrict { io.Reader, io.Writer, *bytes.Buffer }

متكافئة.

بطريقة مماثلة،

interface{} restrict { error, net.Error }

يعادل

interface { Error() string }

منح

type IO interface{} restrict { io.Reader, io.Writer }
type R interface{} restrict {
  interface{} restrict { int, uint },
  IO,
}

ثم النوع الأساسي R يعادل

interface{} restrict { io.Writer, uint, io.Reader, int }

تحرير: تصحيح صغير بالخط المائل

jimmyfrasche لن أذهب إلى حد القول إن ما كتبته أعلاه كان اقتراحًا. كانت أشبه بفكرة. يجب أن أفكر في تعليقاتك ، لكنها تبدو معقولة للوهلة الأولى.

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

الشيء الوحيد الذي سأفكر في تغييره هو تغيير العلامة: يبدو أن foo.X = 0 قد يكون foo = Foo{X: 0} ؛ عدد قليل من الأحرف ، ولكن بشكل أكثر وضوحًا هو إعادة تعيين العلامة وتصفير القيمة. هذه نقطة ثانوية ، وسأكون سعيدًا جدًا إذا تم قبول اقتراحه كما هو.

@ ns-cweber أشكركم ولكني لا أستطيع أن أحصل على الفضل في سلوك القيمة الصفرية. كانت الأفكار تطفو منذ فترة وكانت في اقتراح rogpeppe الذي جاء سابقًا في هذا الموضوع (كما

وبقدر ما يصل إلى foo.X = 0 مقابل foo = Foo{X: 0} ، فإن اقتراحي يسمح لكليهما ، في الواقع. هذا الأخير مفيد إذا كان حقل الاختيار هذا عبارة عن بنية ، لذا يمكنك عمل foo.X.Y = 0 بدلاً من foo = Foo{X: image.Point{X: foo.[X].X, 0}} والذي بالإضافة إلى الإسهاب يمكن أن يفشل في وقت التشغيل.

أعتقد أيضًا أنه يساعد على الاحتفاظ بها على هذا النحو لأنها تعزز درجة المصعد لدلالاتها: إنه هيكل لا يمكن أن يكون له سوى حقل واحد محدد في الوقت.

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

jimmyfrasche شكرًا

أخيرًا ، يبدو اقتراحك البسيط لاختيار التضمين وكأنه اقتراح الحدس. حتى لو غيرنا رأينا ، يمكننا الانتقال من الاقتراح البسيط إلى الاقتراح المعقد دون كسر الكود الحالي ، لكن العكس ليس صحيحًا.

@ ns-cweber

كان بإمكاني أن أرى نفسي أقوم بإعداد foo.XY ، دون أن أدرك أنه سيغير حقل الاختيار تلقائيًا

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

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

قد يكون ذلك مزعجًا عند ارتكابك لخطأ كهذا ، ولكن ، ليس مختلفًا كثيرًا عن "لقد قمت بتعيين bar.X = 0 لكنني كنت أقصد تعيين bar.Y = 0 " لأن الافتراض يعتمد عليك لا تدرك أن foo هو نوع اختيار.

بالمثل i.Foo() ، p.Foo() ، و v.Foo() تبدو جميعها متشابهة ولكن إذا كان i nil واجهة p هو مؤشر صفري و Foo لا يتعامل مع هذه الحالة ، يمكن أن يصاب الأولين بالذعر بينما إذا كان v يستخدم مستقبل طريقة القيمة فإنه لا يمكن (على الأقل ليس من الاستدعاء نفسه ، على أي حال) .

-

فيما يتعلق بالتضمين ، هناك نقطة جيدة حول سهولة فكه لاحقًا ، لذا تقدمت للتو وتحرير الاقتراح.

غالبًا ما تحتوي أنواع المجموع على حقل عديم القيمة. على سبيل المثال ، في الحزمة database/sql ، لدينا:

type NullString struct {
    String string
    Valid  bool // Valid is true if String is not NULL
}

إذا كان لدينا أنواع / اختيارات / اتحادات ، فيمكن التعبير عن ذلك على النحو التالي:

type NullString pick {
  Null   struct{}
  String string
}

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

Bikeshedding (آسف) ، سأجادل بأن هذا يستحق الدعم النحوي وعدم الاتساق مع بناء جملة تضمين حقل البنية:

type NullString union {
  Null
  String string
}

neild

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

type Null = struct{} //though this puts Null in the same scope as NullString
type NullString pick {
  Null
  String string
}

بالعودة إلى وجهة نظرك الرئيسية ، نعم ، هذا استخدام ممتاز. في الواقع ، يمكنك استخدامه لإنشاء تعدادات: type Stoplight pick { Stop, Slow, Go struct{} } . سيكون هذا يشبه إلى حد كبير التعداد الصناعي / ذرة الثعلب. حتى أنه سيتم تجميعها إلى نفس الإخراج. الفائدة الرئيسية في هذه الحالة هي أن الرقم الذي يمثل الحالة مغلف بالكامل ولا يمكنك وضعه في أي حالة بخلاف تلك الثلاثة المدرجة.

لسوء الحظ ، هناك بناء جملة محرج إلى حد ما لإنشاء وتعيين قيم Stoplight والتي تتفاقم في هذه الحالة:

light := Stoplight{Slow: struct{}{}}
light.Go = struct{}{}

السماح لـ {} أو _ بالاختزال لـ struct{}{} ، كما هو مقترح في مكان آخر ، من شأنه أن يساعد.

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

ومع ذلك ، من السهل حل هذه المشكلة باستخدام منشئ رمز يقوم بإنشاء func بالاسم نفسه لكل حقل في الاختيار الذي يأخذ نوع الحقل كوسيطة. إذا كان أيضًا ، كحالة خاصة ، لم يأخذ أي وسيطات إذا كان النوع بحجم صفر ، فسيبدو ناتج المثال Stoplight هكذا

func Stop() Stoplight {
  return Stoplight{Stop: struct{}{}}
}
func Slow() Stoplight {
  return Stoplight{Slow: struct{}{}}
}
func Go() Stoplight {
  return Stoplight{Go: struct{}{}}
}

ومثالك NullString سيبدو كالتالي:

func Null() NullString {
  return NullString{Null: struct{}{}}
}
func String(s string) NullString {
  return NullString{String: s}
}

إنها ليست جميلة ، لكنها على بعد go generate ومن المحتمل أن تكون مضمنة بسهولة.

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

المزيد من بناء الجملة bikeshedding:

type NullString union {
  Null
  Value string
}

var _ = NullString{Null}
var _ = NullString{Value: "some value"}
var _ = NullString{Value} // equivalent to NullString{Value: ""}.

بشكل ملموس ، يتم تفسير القائمة الحرفية مع قائمة العناصر التي لا تحتوي على مفاتيح على أنها تسمية للحقل المراد تعيينه.

سيكون هذا غير متسق من الناحية التركيبية مع الاستخدامات الأخرى للحروف المركبة. من ناحية أخرى ، إنه استخدام يبدو معقولًا وبديهيًا في سياق أنواع الاتحاد / الانتقاء / المجموع (بالنسبة لي على الأقل) ، نظرًا لعدم وجود تفسير معقول لمُهيئ الاتحاد بدون مفتاح.

neild

سيكون هذا غير متسق من الناحية التركيبية مع الاستخدامات الأخرى للحروف المركبة.

يبدو هذا سلبيًا كبيرًا بالنسبة لي ، على الرغم من أنه منطقي في السياق.

لاحظ أيضًا أن

var ns NullString // == NullString{Null: struct{}{}} == NullString{}
ns.String = "" // == NullString{String: ""}

للتعامل مع struct{}{} عندما أستخدم map[T]struct{} أرمي

var set struct{}

في مكان ما واستخدم theMap[k] = set ، سيعمل مماثل مع اللقطات

مزيد من التفرغ للدراجات: النوع الفارغ (في سياق أنواع المجموع) يسمى تقليديًا "وحدة" ، وليس "فارغًا".

تضمين التغريدة

في اللغات الوظيفية ، عندما تنشئ نوع مجموع ، فإن تسمياته هي في الواقع وظائف تنشئ قيم هذا النوع ، (وإن كانت وظائف خاصة تُعرف باسم "مُنشئ النوع" أو "tycons" التي يعرفها المترجم للسماح بمطابقة الأنماط) ، لذلك

data Bool = False | True

يقوم بإنشاء نوع البيانات Bool ووظيفتين في نفس النطاق ، True و False ، كل واحدة تحمل التوقيع () -> Bool .

هنا () هو كيف تكتب نوع الوحدة المنطوقة - النوع الذي له قيمة واحدة فقط. في Go ، يمكن كتابة هذا النوع بعدة طرق مختلفة ، لكنه مكتوب بشكل اصطلاحي كـ struct{} .

لذا فإن نوع وسيطة المنشئ يسمى الوحدة. اصطلاح اسم المُنشئ بشكل عام هو None عند استخدامه كنوع خيار مثل هذا ، ولكن يمكن تغييره ليناسب المجال. Null سيكون اسمًا جيدًا إذا كانت القيمة قادمة من قاعدة بيانات ، على سبيل المثال.

تضمين التغريدة

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

بالنسبة إلى منظور بديل ، أرى أن هذا يمثل عيبًا رئيسيًا لأنواع المجموع في Go.

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

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

إذا كان الرمز الجديد يستخدم أنواع المجموع لإرجاع مجموعات (قيمة ، خطأ) ، فسيصبح هذا النظام البيئي مجزأًا.

إذا أضفنا أنواع المجموع في Go 2 واستخدمناها بشكل موحد ، فإن المشكلة تقلل إلى واحدة من الترحيل ، وليس التجزئة: يجب أن يكون من الممكن تحويل Go 1 (قيمة ، خطأ) API إلى Go 2 (قيمة | خطأ ) API والعكس صحيح ، ولكن يمكن أن تكون أنواع مميزة في أجزاء Go 2 من البرنامج.

إذا أضفنا أنواع المجموع في Go 2 واستخدمناها بشكل موحد

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

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

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

type ReadResult pick(N int, Err error) {
    N
    PartialResult struct { N; Err }
    Err
}

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

type ReadResult pick(N int, Err error) {
case Err == nil:
    N
default:
    PartialResult struct { N; Err }
case N == 0:
    Err
}

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

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

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

هذا حقًا مجرد مثال لذلك هنا

الإصدار 1 بدون مبالغ

func Take(i interface{}) error {
  switch i.(type) {
  case int: //do something
  case string:
  default: return fmt.Errorf("invalid %T", i)
  }
}
func Give() (interface{}, error) {
   i := f() //something
   if i == nil {
     return nil, errors.New("whoops v:)v")
  }
  return i
}

الإصدار 2 بالمبالغ

type Value pick {
  I int
  S string
}
func TakeSum(v Value) {
  // do something
}
// Deprecated: use TakeSum
func Take(i interface{}) error {
  switch x := i.(type) {
  case int: TakeSum(Value{I: x})
  case string: TakeSum(Value{S: x})
  default: return fmt.Errorf("invalid %T", i)
  }
}
type ErrValue pick {
  Value
  Err error
}
func GiveSum() ErrValue { //though honestly (Value, error) is fine
  return f()
}
// Deprecated: use GiveSum
func Give() (interface{}, error) {
  switch v := GiveSum().(var) {
  case Value:
    switch v := v.(var) {
    case I: return v, nil
    case S: return v, nil
    }
  case Err:
    return nil, v
  }
}

الإصدار 3 من شأنه إزالة العطاء / خذ

الإصدار 4 سيحرك تنفيذ GiveSum / TakeSum إلى العطاء / الاستلام ، وجعل GiveSum / TakeSum فقط اتصل بـ Give / Take ثم قم بإهمال GiveSum / TakeSum.

الإصدار 5 سيزيل GiveSum / TakeSum

إنها ليست جميلة أو سريعة ولكنها تشبه أي اضطراب آخر واسع النطاق له طبيعة مماثلة ولا يتطلب شيئًا إضافيًا من اللغة

أعتقد (معظم) فائدة نوع المجموع يمكن أن تتحقق من خلال آلية لتقييد التخصيص لنوع من واجهة النوع {} في وقت الترجمة.

في أحلامي تبدو كالتالي:

type T1 switch {T2,T3} // only nil, T2 and T3 may be assigned to T1
type T2 struct{}
type U switch {} // only nil may be assigned to U
type V switch{interface{} /* ,... */} // V equivalent to interface{}
type Invalid switch {T2,T2} // only uniquely named types
type T3 switch {int,uint} // switches can contain switches but... 

... سيكون أيضًا خطأ في وقت الترجمة لتأكيد أن نوع التبديل هو نوع غير محدد صراحة:

var t1 T1
i,ok := t1.(int) // T1 can't be int, only T2 or T3 (but T3 could be int)
switch t := t1.(type) {
    case int: // compile error, T1 is just nil, T2 or T3
}

ويذهب الطبيب البيطري إلى التعيينات الثابتة الغامضة لأنواع مثل T3 ولكن لجميع المقاصد والأغراض (في وقت التشغيل) var x T3 = 32 سيكون var x interface{} = 32 . ربما تكون بعض أنواع المفاتيح المحددة مسبقًا للمبنى في حزمة تسمى شيئًا مثل المفاتيح أو المهور رائعة أيضًا.

@ j7b ، عرض ianlancetaylor فكرة مماثلة في https://github.com/golang/go/issues/19412#issuecomment -323256891

لقد نشرت ما أعتقد أنه سيكون النتائج المنطقية لهذا لاحقًا على https://github.com/golang/go/issues/19412#issuecomment -325048452

يبدو أن الكثير منهم سيطبق بالتساوي بالنظر إلى التشابه.

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

لكنني لا أعتقد أنه من الممكن إنجاحها.

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

ربما أفتقد شيئًا ما.

لا حرج في اقتراح الواجهة المقيدة طالما أنك على ما يرام مع الحالات التي لا تكون بالضرورة مفككة. لا أعتقد أنه من المفاجئ كما تفعل أن الاتحاد بين نوعين من الواجهات (مثل io.Reader / io.Writer ) ليس مفككًا. إنه يتفق تمامًا مع حقيقة أنه لا يمكنك تحديد ما إذا كانت القيمة المعينة لـ interface{} قد تم تخزينها على أنها io.Reader أو io.Writer إذا كانت تنفذ كليهما. حقيقة أنه يمكنك بناء اتحاد منفصل طالما أن كل حالة من النوع الملموس تبدو مناسبة تمامًا.

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

jimmyfrasche مقابل type T switch {io.Reader,io.Writer} لا بأس من تعيين ReadWriter إلى T ولكن يمكنك فقط التأكيد على أن T هو قارئ io.Reader أو Io.Writer ، فأنت بحاجة إلى تأكيد آخر لتأكيد io.Reader أو io.Writer هو ReadWriter ، والذي يجب أن يشجع إضافته إلى switchtype إذا كان تأكيدًا مفيدًا.

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

ومن ناحية أخرى ، فإن تركيبianlancetaylor يسمح بذلك

type IR interface {
  Foo()
  Bar()
} restrict { A, B, C }

والتي يمكن تجميعها طالما أن A و B و C لكل منها طرق Foo و Bar (على الرغم من أنه يجب أن تقلق حول قيم nil ).

تحرير: توضيح بخط مائل

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

//MyGroup can be any of these. It can contain other groups, interfaces, structs, or primitive types
type MyGroup group {
   MyOtherGroup
   MyInterface
   MyStruct
   int
   string
   //..possibly some other types as well
}

//type definitions..
type MyInterface interface{}
type MyStruct struct{}
//etc..

func DoWork(item MyGroup) {
   switch t:=item.(type) {
      //do work here..
   }
}

هناك العديد من الفوائد لهذا الأسلوب على طريقة الواجهة الفارغة التقليدية interface{} :

  • التحقق من النوع الثابت عند استخدام الوظيفة
  • يمكن للمستخدم استنتاج نوع الوسيطة المطلوبة من توقيع الوظيفة وحده ، دون الحاجة إلى إلقاء نظرة على تنفيذ الوظيفة

تكون الواجهة الفارغة interface{} مفيدة عندما يكون عدد الأنواع المعنية غير معروف. ليس لديك حقًا خيار هنا سوى الاعتماد على التحقق من وقت التشغيل. من ناحية أخرى ، عندما يكون عدد الأنواع محدودًا ومعروفًا أثناء وقت الترجمة ، فلماذا لا نجعل المترجم يساعدنا؟

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

إليك تقرير تجربة بخصوص Go protobufs:

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

    • هذا غير مؤدي بسبب تخصيص إضافي (على الرغم من أن النقابات قد تعاني من نفس المصير حسب التنفيذ).
    • هذا أمر مؤلم للمستخدمين لأن الحاجة إلى التحقق باستمرار من المؤشر تضر بالقراءة (على الرغم من أن القيم الافتراضية غير الصفرية في البروتوس قد تعني أن الحاجة إلى التحقق أمر جيد ...).
  • تسمح اللغة الأولية بـ "one ofs" ، وهي نسخة أولية من أنواع المجموع. النهج المتبع حاليًا هو كما يلي ( مثال إجمالي ):

    • تحديد نوع الواجهة بطريقة مخفية (على سبيل المثال ، type Communique_Union interface { isCommunique_Union() } )
    • لكل نوع من أنواع Go المحتملة المسموح بها في الاتحاد ، حدد بنية مجمعة ، والغرض الوحيد منها هو التفاف كل نوع مسموح به (على سبيل المثال ، type Communique_Number struct { Number int32 } ) حيث يكون لكل نوع طريقة isCommunique_Union .
    • هذا أيضًا غير مؤدٍ لأن الأغلفة تسبب تخصيصًا. سيساعد نوع المجموع لأننا نعلم أن أكبر قيمة (شريحة) لن تشغل أكثر من 24B.

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

تقصد بإضافة طريقة وهمية غير مصدرة إلى كائن بحيث يمكن تمرير الكائن كواجهة ، على النحو التالي؟

type MyInterface interface {
   belongToMyInterface() //dummy method definition
}

type MyObject struct{}
func (MyObject) belongToMyInterface(){} //dummy method

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

هذه هي مشاكل الطريقة الدمية:

  • تعاريف الأساليب والطرق غير الضرورية تشوش الكائن والواجهة.
  • في كل مرة يتم فيها إضافة _group_ جديدة ، تحتاج إلى تعديل تنفيذ الكائن (على سبيل المثال ، إضافة طرق وهمية). هذا خطأ (انظر النقطة التالية).
  • نوع البيانات الجبرية (أو التجميع على أساس _domain_ بدلاً من السلوك) خاص بالمجال . اعتمادًا على المجال ، قد تحتاج إلى عرض علاقة الكائن بشكل مختلف. يقوم المحاسب بتجميع المستندات بشكل مختلف عن مدير المستودع. هذه المجموعة تتعلق بمستهلك الكائن ، وليس الكائن نفسه. لا يحتاج الكائن إلى معرفة أي شيء عن مشكلة المستهلك ، ولا يحتاج إلى ذلك. هل تحتاج الفاتورة إلى معرفة أي شيء عن المحاسبة؟ إذا لم يحدث ذلك ، فلماذا تحتاج الفاتورة إلى تغيير تنفيذها _ (على سبيل المثال ، إضافة طرق وهمية جديدة) _ في كل مرة يحدث تغيير في قاعدة المحاسبة _ (على سبيل المثال ، تطبيق تجميع مستندات جديد) _؟ باستخدام طريقة _dummy_ ، يمكنك إقران الكائن الخاص بك بمجال المستهلك وإجراء افتراضات مهمة حول مجال المستهلك. لا يجب عليك القيام بذلك. هذا أسوأ من طريقة interface{} للواجهة الفارغة. هناك طرق أفضل المتاحة.

تضمين التغريدة

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

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

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

ينتهي بك الأمر بالعديد من الطرق الفارغة المرفقة بالكائنات ، وليس من الواضح للقراء سبب احتياجك لهذه الأساليب لأن الواجهات التي تتطلبها تعيش في مجال / حزمة / طبقة منفصلة.

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

ما زلت أرى جانبين سلبيين للمبالغ المغلقة كما تم تنفيذها بلغات أخرى: الأول ، صعوبة تطويرها في عملية تطوير موزعة على نطاق واسع. وثانيًا ، أعتقد أنهم يضيفون قوة إلى نظام النوع وأنا أحب أن Go ليس لديه نظام نوع قوي جدًا ، لأن ذلك لا يشجع أنواع الترميز وبدلاً من ذلك برامج الكود - عندما أشعر أن مشكلة يمكن أن تستفيد من نظام كتابة أكثر قوة ، أنتقل إلى لغة أكثر قوة (مثل Haskell أو Rust).

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

[1] على وجه الخصوص ، هذا ليس بالأمر السهل ، لكنه لا يزال ممكنًا ، على سبيل المثال يمكنك القيام به

type Node interface {
    node()
}

type Foo struct {
    bar.Baz
}

func (foo) node() {}

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

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

أنا لا أتفق مع نقطتك السلبية الثانية.

هل لا توافق على عبارة "أنواع الجمع تشجع البرمجة بأنواع" ، أم أنك لا توافق على أن ذلك يمثل جانبًا سلبيًا؟ نظرًا لأنه لا يبدو أنك لا تتفق مع الأول (تعليقك هو في الأساس مجرد إعادة تأكيد لذلك) وفيما يتعلق بالثاني ، فقد أقرت بأن الأمر متروك للتفضيل أعلاه.

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

هذا النوع من حجة الأبيض والأسود لا يساعد حقًا . أوافق على أن أنواع الجمع هذه تقلل الألم في بعض الحالات. كل تغيير يجعل نظام الكتابة أكثر قوة سيقلل من الألم في بعض الحالات - ولكنه سيسبب أيضًا الألم في بعض الحالات. لذا فإن السؤال هو ، أيهما يفوق الآخر (وهذا ، إلى درجة جيدة ، مسألة تفضيل).

لا ينبغي أن تكون المناقشات حول ما إذا كنا نريد نظام نوع python-esque (بدون أنواع) أو نظام نوع coq-esque (أدلة صحة لكل شيء). يجب أن تكون المناقشة "هل تفوق فوائد أنواع المجموع سلبياتها" ومن المفيد الاعتراف بكليهما.


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

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

سؤال اخر:

حقيقة أن هناك الكثير من الأماكن في المكتبة القياسية التي من شأنها أن تستفيد بشكل كبير من أنواع المجموع

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

قد يستفيد database/sql/driver.Value من كونه نوع مجموع (كما هو مذكور في # 23077).
https://godoc.corp.google.com/pkg/database/sql/driver#Value

ومع ذلك ، فإن الواجهة الأكثر عمومية في database/sql.Rows.Scan لن تؤدي إلى فقدان الوظائف. يمكن للمسح قراءة القيم التي يكون نوعها الأساسي على سبيل المثال ، int ؛ يتطلب تغيير معلمة الوجهة الخاصة به إلى نوع المجموع قصر مدخلاته على مجموعة محدودة من الأنواع.
https://godoc.corp.google.com/pkg/database/sql#Rows.Scan

تضمين التغريدة

لن أكون معارضة لأنواع المجموع المفتوح (أي أن كل نوع مجموع له حالة "SomethingElse" ضمنية أو صريحة) ، لأنه سيخفف من معظم الجوانب الفنية لها (غالبًا ما يكون من الصعب تطويرها)

هناك خياران آخران على الأقل يخففان من مشكلة "صعوبة التطور" للمبالغ المغلقة.

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

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

قد يستفيد database/sql/driver.Value من كونه نوع المجموع

متفق عليه ، لم أكن أعرف عن ذلك. شكرا :)

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

حل مثير للاهتمام.

واجهات Merovius هي في الأساس عائلة من أنواع مجموع لانهائي. جميع أنواع المجموع ، لانهائية أو غير ذلك ، لها حالة default: . مع ذلك ، بدون أنواع المبالغ المحددة ، يعني default: إما حالة صالحة لم تكن على علم بها أو حالة غير صالحة تمثل خطأ في مكان ما في البرنامج - بالمبالغ المحدودة ، تكون الحالة الأولى فقط وليست الأخيرة على الإطلاق.

تعد أنواع json.Token و sql.Null * أمثلة أساسية أخرى. الذهاب / الأنواع ستستفيد بنفس الطريقة التي يعمل بها go / ast. أعتقد أن هناك الكثير من الأمثلة غير الموجودة في واجهات برمجة التطبيقات التي تم تصديرها حيث سيكون من الأسهل تصحيح الأخطاء واختبار بعض السباكة المعقدة من خلال تقييد مجال الحالة الداخلية. أجدها مفيدة للغاية للقيود الداخلية للحالة والتطبيق التي لا تظهر كثيرًا في واجهات برمجة التطبيقات العامة للمكتبات العامة ، على الرغم من أن لها استخدامات عرضية هناك أيضًا.

أنا شخصياً أعتقد أن أنواع المجموع تعطي Go ما يكفي من القوة الإضافية ولكن ليس كثيرًا. يعد نظام النوع Go بالفعل لطيفًا ومرنًا للغاية ، على الرغم من وجود عيوبه. لن توفر إضافات Go2 إلى نظام الكتابة قدرًا كبيرًا من الطاقة كما هو موجود بالفعل - 80-90٪ مما هو مطلوب موجود بالفعل. أعني ، حتى الأدوية الجنيسة لن تسمح لك بشكل أساسي بفعل شيء جديد: إنها ستتيح لك القيام بأشياء تقوم بها بالفعل بأمان أكبر ، وبسهولة أكبر ، وبطريقة أفضل ، وبطريقة تتيح لك استخدام أدوات أفضل. أنواع المجموع متشابهة ، imo (على الرغم من أنه من الواضح أنه إذا كانت واحدة أو أخرى ، فستكون لها الأسبقية (وهي تتزاوج بشكل جيد إلى حد ما)).

إذا سمحت بوضع افتراضي غريب (كل الحالات + الإعداد الافتراضي مسموح به) على مفاتيح التحويل من نوع المجموع ولم يكن لديك المترجم يفرض الاستنفاد (على الرغم من أن linter يمكن) ، فإن إضافة حالة إلى المجموع تكون بنفس السهولة (وبنفس الصعوبة ) كتغيير أي واجهة برمجة تطبيقات عامة أخرى.

تعد أنواع json.Token و sql.Null * أمثلة أساسية أخرى.

رمز - بالتأكيد. مثال آخر لمشكلة AST (يستفيد أي محلل من أنواع المجموع بشكل أساسي).

لا أرى فائدة sql.Null * ، بالرغم من ذلك. بدون الأدوية الجنيسة (أو إضافة بعض العناصر المضمنة الاختيارية العامة "السحرية") ، لا يزال يتعين عليك الحصول على الأنواع ولا يبدو أن هناك فرقًا كبيرًا بين type NullBool enum { Invalid struct{}; Value Int } و type NullBool struct { Valid bool; Value Int } . نعم ، أدرك أن هناك فرقًا ، لكنه صغير جدًا.

إذا سمحت بوضع افتراضي غريب (كل الحالات + الإعداد الافتراضي مسموح به) على مفاتيح التحويل من نوع المجموع ولم يكن لديك المترجم يفرض الاستنفاد (على الرغم من أن linter يمكن) ، فإن إضافة حالة إلى المجموع تكون بنفس السهولة (وبنفس الصعوبة ) كتغيير أي واجهة برمجة تطبيقات عامة أخرى.

أنظر فوق. هذه هي ما أسميه بالمبالغ المفتوحة ، فأنا أقل معارضة لها.

هذه هي ما أسميه بالمبالغ المفتوحة ، فأنا أقل معارضة لها.

اقتراحي المحدد هو https://github.com/golang/go/issues/19412#issuecomment -323208336 وأعتقد أنه قد يلبي تعريفك للفتح ، على الرغم من أنه لا يزال تقريبيًا بعض الشيء وأنا متأكد من أن هناك المزيد إزالة وتلميع. على وجه الخصوص ، لاحظت أنه لم يكن من الواضح أن الحالة الافتراضية مقبولة حتى لو تم سرد جميع الحالات ، لذلك قمت بتحديثها للتو.

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

type Nullable(T) pick { // or whatever syntax (on all counts)
  Null struct{}
  Value T
}

مرة واحدة وستكون تغطية جميع الحالات أمرًا رائعًا. ولكن ، كما أشرت أيضًا ، يمكننا فعل الشيء نفسه مع منتج عام (هيكل). هناك حالة غير صالحة لـ Valid = false ، Value! = 0. في هذا السيناريو ، سيكون من السهل استئصال ما إذا كان ذلك يسبب مشكلات نظرًا لأن 2 ⨯ T صغير ، حتى لو لم يكن صغيرًا مثل 1 + T.

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

رمز - بالتأكيد. مثال آخر لمشكلة AST (يستفيد أي محلل من أنواع المجموع بشكل أساسي).

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

عندما أكتب مكتبة عامة ، لا تظهر في واجهة برمجة التطبيقات كثيرًا مثل القيام ببعض ETL أو إعداد تقرير خيالي أو التأكد من أن المستخدمين في الحالة X لديهم إجراء Y يحدث إذا لم يتم وضع علامة Z. حتى في مكتبة عامة على الرغم من أنني أجد أماكن يمكن أن تكون فيها القدرة على تقييد الحالة الداخلية مفيدة ، حتى لو قللت من تصحيح الأخطاء لمدة 10 دقائق إلى ثانية واحدة "أوه ، قال المترجم إنني مخطئ".

مع Go في مكان معين حيث أستخدم أنواع sum ، هو اختيار goroutine عبر مجموعة من القنوات حيث أحتاج إلى إعطاء 3 قنوات إلى goroutine واحد و 2 إلى آخر. سيساعدني ذلك في تتبع ما يحدث لأتمكن من استخدام chan pick { a A; b B; c C } على chan A ، chan B ، chan C الرغم من chan stuct { kind MsgKind; a A; b B; c C } يمكن قم بالمهمة في قرصة على حساب مساحة إضافية ونسبة أقل من التحقق من الصحة.

بدلاً من نوع جديد ، ماذا عن فحص قائمة نوع وقت الترجمة كإضافة إلى ميزة تبديل نوع الواجهة الحالية؟

func main() {
    if FlipCoin() == false {
        printCertainTypes(FlipCoin(), int(5))
    } else {
        printCertainTypes(FlipCoin(), string("5"))
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        default:
            fmt.Println(v)
        }
    } else {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case string:
            fmt.Printf(“string %v\n”, v)
        }
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)   
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
    fmt.Println(flip)
    switch v := in.(type) {
    case string:
        fmt.Printf(“string %v\n”, v)
    case bool:
        fmt.Printf(“bool 2 %v\n”, v)
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    } else {
        switch v := in.(type) {
        case string:
            fmt.Printf(“string %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    fmt.Println(flip)
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
}

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

الوسيلة القياسية هي واجهة مع طريقة غير مُصدرة ، لا تفعل شيئًا كعلامة.

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

type Sum interface { sum() }
type sum struct{}
func (sum) sum() {}

وقم فقط بتضمين علامة العرض 0 في هياكلنا.

يمكننا إضافة أنواع خارجية لمجموعنا من خلال تقديم غلاف

type External struct {
  sum
  *pkg.SomeType
}

على الرغم من أن هذا صعب بعض الشيء.

إذا كان جميع الأعضاء في المجموع يشتركون في سلوك مشترك ، فيمكننا تضمين هذه الأساليب في تعريف الواجهة.

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

import "p"
var member struct {
  p.Sum
}

داخل الحزمة ، يجب أن نحرص على التحقق من صحة القيم التي يتم تجميعها ولكنها غير قانونية.

هناك طرق مختلفة لاستعادة بعض أنواع الأمان في وقت التشغيل. لقد اكتشفت تضمين طريقة valid() error في تعريف واجهة sum إلى جانب func like

func valid(s Sum) error {
  switch s.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case A, B, C, ...: // listing each valid member
    return s.valid()
  }
  return fmt.Errorf("pkg: %T is not a valid member of Sum")
}

لتكون مفيدة لأنها تتيح الاهتمام بنوعين من التحقق في وقت واحد. بالنسبة للأعضاء الذين يكونون دائمًا صالحين ، يمكننا تجنب بعض النماذج المعيارية مع

type alwaysValid struct{}
func (alwaysValid) valid() error { return nil }

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

//A Node is one of (list of types).
type Node interface { node() }

اكتب

//A Node is only valid if it is defined in this package.
type Node interface { 
  //Node is a dummy method that signifies that a type is a Node.
  Node()
}

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

يكون هذا النمط مفيدًا عندما يتم تحديد غالبية الأنواع في المجموع في نفس الحزمة. في حالة عدم وجود أي منها ، فإن اللجوء الشائع هو الرجوع إلى interface{} ، مثل json.Token أو driver.Value . يمكننا استخدام النمط السابق مع أنواع الغلاف لكل منها ولكن في النهاية تقول ما يصل إلى interface{} لذلك هناك نقطة صغيرة. إذا كنا نتوقع أن تأتي هذه القيم من خارج الحزمة ، فيمكننا أن نكون مهذبين ونحدد المصنع:

//Sum is one of int64, float64, or bool.
type Sum interface{}
func New(v interface{}) (Sum, error) {
  switch v.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case int64, float64, bool:
     return v
  }
  return fmt.Printf("pkg: %T is not a valid member of Sum")
}

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

*T لنشير إلى عدم وجود قيمة كمؤشر nil وقيمة صفرية (ربما) كنتيجة لإلغاء تحديد مؤشر غير صفري.

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

بالنسبة إلى الاختيارات ، يمكن تجنب ذلك باستخدام التقنية الموجودة في حزمة sql

type OptionalT struct {
  Valid bool
  Value T
}

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

إن أبسط أشكال أنواع المجموع هو عندما تهتم بالهوية ، وليس القيمة: التعداد.

الطريقة التقليدية للتعامل مع هذا في Go هي const / iota:

type Enum int
const (
  A Enum = iota
  B
  C
)

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

هناك أيضًا مشكلة العدد الأساسي لهذا النوع. A+B == C . يمكننا تحويل الثوابت التكاملية غير المكتوبة إلى هذا النوع بسهولة بالغة. هناك الكثير من الأماكن التي يكون فيها هذا مرغوبًا فيه ، لكننا نحصل عليه بغض النظر عن السبب. مع القليل من العمل الإضافي ، يمكننا قصر هذا على الهوية فقط:

type Enum struct { v int }
var (
  A = Enum{0}
  B = Enum{1}
  C = Enum{2}
)

الآن هذه مجرد علامات مبهمة. يمكن مقارنتها ولكن هذا كل شيء. لسوء الحظ ، فقدنا الآن الثبات ، لكن يمكننا استعادتها بقليل من العمل:

func A() Enum { return Enum{0} }
func B() Enum { return Enum{1} }
func C() Enum { return Enum{2} }

لقد استعدنا عدم قدرة مستخدم خارجي على تغيير الأسماء على حساب بعض النماذج المعيارية وبعض استدعاءات الوظائف التي تكون مضمنة للغاية.

ومع ذلك ، يعد هذا من بعض النواحي أجمل من مبالغ الواجهة نظرًا لأننا أغلقنا النوع بالكامل تقريبًا. يمكن للرمز الخارجي استخدام A() أو B() أو C() . لا يمكنهم تبديل الملصقات كما في المثال var ولا يمكنهم فعل A() + B() ونحن أحرار في تحديد أي طريقة نريدها على Enum . سيظل من الممكن للكود في نفس الحزمة إنشاء قيمة أو تعديلها بشكل خاطئ ، ولكن إذا حرصنا على ضمان عدم حدوث ذلك ، فهذا هو نوع المجموع الأول الذي لا يتطلب رمز التحقق: إذا كان موجودًا ، فهو صالح .

في بعض الأحيان يكون لديك العديد من الملصقات وبعضها يحتوي على تاريخ إضافي وتلك التي تحتوي على نفس النوع من البيانات. لنفترض أن لديك قيمة تحتوي على ثلاث حالات لا قيمة لها (A ، B ، C) ، واثنتان لها قيمة سلسلة (D ، E) وواحدة ذات قيمة سلسلة وقيمة int (F). يمكننا استخدام عدد من المجموعات من التكتيكات المذكورة أعلاه ، ولكن أبسط طريقة هي

type Value struct {
  Which int // could have consts for A, B, C, D, E, F
  String string
  Int int
}

هذا يشبه إلى حد كبير النوع OptionalT أعلاه ، ولكن بدلاً من منطقي ، فإنه يحتوي على تعداد وهناك عدة حقول يمكن تعيينها (أو لا) اعتمادًا على قيمة Which . يجب أن يكون التحقق من الصحة حريصًا على تعيينها (أو عدم تعيينها) بشكل مناسب.

هناك العديد من الطرق للتعبير عن "واحد مما يلي" في Go. يحتاج البعض إلى رعاية أكثر من البعض الآخر. غالبًا ما تتطلب التحقق من صحة "واحد" الثابت في وقت التشغيل أو الانحرافات الخارجية. يتمثل أحد الجوانب السلبية الرئيسية التي يتشاركونها جميعًا في أنه نظرًا لأنهم يتم محاكاتهم في اللغة بدلاً من أن يكونوا جزءًا من اللغة ، فإن "واحد من" الثابت لا يظهر في الانعكاس أو الانتقال / الأنواع ، مما يجعل من الصعب إجراء metaprogram باستخدام معهم. لاستخدامها في metaprogramming ، عليك أن تكون قادرًا على التعرف على النكهة الصحيحة لـ sum والتحقق من صحتها وإخبارك أن هذا هو ما تبحث عنه نظرًا لأنها تشبه إلى حد كبير رمزًا صالحًا بدون "واحد" الثابت.

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

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

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

ومع ذلك ، يمكنك منع هذه المشكلة ، عن طريق تغيير الواجهة إلى type Sum interface { sum() Sum } وإرجاع كل قيمة بنفسها. بهذه الطريقة ، يمكنك فقط استخدام عائد sum() ، والذي سيكون حسن التصرف حتى في ظل التضمين.

إحدى الشكاوى الأكثر شيوعًا حول هذا النمط هي أنه لا يجعل العضوية في المجموع واضحة في godoc.

هذا قد يساعدك .

يتمثل الجانب السلبي الرئيسي لهذا في أنه يسمح بتشفير حالة غير صالحة: يمكن أن تكون القيمة الصالحة خاطئة ويمكن أن تكون القيمة غير صفرية.

هذه ليست حالة غير صالحة بالنسبة لي. القيم الصفرية ليست سحرية. لا يوجد فرق ، IMO ، بين sql.NullInt64{false,0} و NullInt64{false,42} . كلاهما تمثيل صالح ومكافئ لـ SQL NULL. إذا تم التحقق من صحة جميع التعليمات البرمجية قبل استخدام القيمة ، فإن الاختلاف لا يمكن ملاحظته للبرنامج.

إنه نقد عادل وصحيح أن المترجم لا يفرض إجراء هذا الفحص (وهو ما قد يفعله ، بالنسبة إلى الاختيارات / أنواع المجموع "الحقيقية") ، مما يسهل عدم القيام بذلك. ولكن إذا نسيت ذلك ، فلن أعتبر أنه من الأفضل استخدام قيمة صفرية عن طريق الخطأ بدلاً من استخدام قيمة غير صفرية بطريق الخطأ (مع استثناء محتمل للأنواع على شكل مؤشر ، لأنها ستصاب بالذعر عند استخدامها ، وبالتالي فشل بصوت عالٍ - ولكن بالنسبة لهؤلاء ، يجب عليك فقط استخدام النوع المجرد على شكل مؤشر على أي حال واستخدام nil كـ "unset").

هناك أيضًا مشكلة العدد الأساسي لهذا النوع. A + B == C. يمكننا تحويل الثوابت المتكاملة غير المكتوبة إلى هذا النوع بسهولة بالغة.

هل هذا اهتمام نظري أم أنه ظهر في الممارسة؟

يمكن للمبرمجين التركيز على رمز التحقق المهم بدلاً من الصيانة التافهة للتحقق من أن القيمة موجودة بالفعل في المجال الصحيح.

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

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

الوسيلة القياسية هي واجهة مع طريقة غير مُصدرة ، لا تفعل شيئًا كعلامة.

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

من ناحية أخرى ، وجود شيء مثل:

func foo(val string|int|error) {
    switch v:= val.(type) {
    case string:
        ...
    }
}

يمنح المستهلك القوة الكاملة في استخدام قيمة نوع المجموع. إنها قيمة ملموسة وليست مفتوحة للتأويل.

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

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

يبدو هذا وكأنه حجة ضعيفة جدًا بالنسبة لي - إذا لم يكن هناك شيء آخر ، فلأنها موجودة ، فأنت تسمح بالفعل بكل ما تمكّنه. في الواقع ، لدينا بالفعل حجج اختيارية ، لجميع المقاصد والأغراض (لا يعجبني هذا النمط ، لكن من الواضح أنه ممكن بالفعل في اللغة).

هناك اختلاف جوهري بين الواجهات وأنواع المجموع (لم أره مذكورًا في رسالتك). عندما تقوم بتقريب نوع المجموع عبر واجهة ، فلا توجد طريقة للتعامل مع القيمة. بصفتك مستهلكًا ، ليس لديك أي فكرة عما يحمله في الواقع ، ولا يمكنك إلا التخمين.

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

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

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

تضمين التغريدة

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

أنا لا أتعامل مع هذا على أنه شيء دائمًا أو أبدًا .

إذا كان شخص ما يمر بإدخال خاطئ ينفجر على الفور ، فأنا لا أزعجني برمز التحقق.

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

يعد قضاء نصف دقيقة الآن في توفير نصف ساعة من تصحيح الأخطاء لاحقًا أمرًا عمليًا. خاصة بالنسبة لي لأنني أرتكب أخطاء غبية طوال الوقت وكلما تعلمت مبكرًا ، يمكنني المضي قدمًا في وقت مبكر لارتكاب الخطأ الغبي التالي.

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

تحليل Godoc

أنا على علم ولكني لا أجده مفيدًا. تم تشغيله لمدة 40 دقيقة على مساحة العمل الخاصة بي قبل أن أضغط على ^ C ويجب تحديث ذلك في كل مرة يتم فيها تثبيت حزمة أو تعديلها. هناك # 20131 (متشعب من هذا الموضوع بالذات!) بالرغم من ذلك.

ومع ذلك ، يمكنك منع هذه المشكلة ، عن طريق تغيير الواجهة إلى type Sum interface { sum() Sum } وإرجاع كل قيمة بنفسها. بهذه الطريقة ، يمكنك فقط استخدام عائد sum() ، والذي سيكون حسن التصرف حتى في ظل التضمين.

لم أجد ذلك مفيدًا. لا يوفر أي فوائد أكثر من التحقق الصريح ويوفر قدرًا أقل من التحقق من الصحة.

هل [حقيقة أنه يمكنك إضافة أعضاء في تعداد ثابت / ذرة] شاغل نظري أم أنه تم طرحه عمليًا؟

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

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

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

هذه ليست حالة غير صالحة بالنسبة لي. القيم الصفرية ليست سحرية. لا يوجد فرق ، IMO ، بين sql.NullInt64{false,0} و NullInt64{false,42} . كلاهما تمثيل صالح ومكافئ لـ SQL NULL. إذا تم التحقق من صحة جميع التعليمات البرمجية قبل استخدام القيمة ، فإن الاختلاف لا يمكن ملاحظته للبرنامج.

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

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

وبالتأكيد ، أفضّل *T للأنواع الاختيارية على الرغم من أن ذلك يحتوي على تكاليف غير صفرية مرتبطة به ، سواء في وقت التنفيذ أو في قابلية قراءة الكود.

(بالنسبة لهذا المثال المحدد ، سيكون الرمز للحصول على القيمة الفعلية أو القيمة الصفرية الصحيحة مع اقتراح الاختيار هو v, _ := nullable.[Value] وهو موجز وآمن.)

هذا ليس ما أريده. يجب أن تكون أنواع الانتقاء من أنواع القيمة ،
مثل كلمة Rust. يجب أن تكون كلمتهم الأولى مؤشرًا إلى GC Metadata ، إذا لزم الأمر.

وإلا فإن استخدامها يأتي مع عقوبة الأداء التي قد تكون كذلك
غير مقبول. بالنسبة لي ، الممر 10:41 صباحًا ، "جوش بليشر سنايدر" <
[email protected]> كتب:

مع اقتراح الاختيار ، يمكنك اختيار أن يكون لديك ap أو * p يمنحك المزيد
تحكم أكبر في مقايضات الذاكرة.

السبب في تخصيص الواجهات لتخزين القيم العددية هو أنك لا تفعل ذلك
يجب أن تقرأ كلمة كتابة لتقرر ما إذا كانت الكلمة الأخرى هي
مؤشر. راجع # 8405 https://github.com/golang/go/issues/8405 من أجل
نقاش. من المحتمل أن تنطبق نفس اعتبارات التنفيذ على أ
اختر النوع ، والذي قد يعني عمليًا أن ينتهي الأمر بالتخصيص والوجود
غير محلي على أي حال.

-
أنت تتلقى هذا لأنك قمت بتأليف الموضوع.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/19412#issuecomment-323371837 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AGGWB-wQD75N44TGoU6LWQhjED_uhKGUks5sZaKbgaJpZM4MTmSr
.

urandom

ماذا يحدث في حالة تصدير الطريقة الوهمية ويمكن لأي طرف ثالث تنفيذ "نوع المجموع"؟

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

type X interface { x() X }
type IntX int
func (v IntX) x() X { return v }
type StringX string
func (v StringX) x() X { return v }
type StructX struct{
    Foo bool
    Bar int
}
func (v StructX) x() X { return v }

لا يوجد امتداد ممكن خارج الحزمة ، ومع ذلك يمكن لمستهلكي الحزمة استخدام القيم وإنشاءها ونقلها تمامًا مثل أي قيم أخرى.

يمكنك تضمين X ، أو أحد الأنواع المحلية التي ترضيه ، خارجيًا ثم تمريره إلى وظيفة في الحزمة الخاصة بك تأخذ X.

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

يبدو أن استدعاء المدقق الذي يثير الفزع برسالة "لا تفعل ذلك" هو الطريقة الأقل إثارة للدهشة للتعامل مع ذلك ويسمح للمتصل بإصلاح شفرته.

إذا استدعى هذا func x ، فسيكون إما ذعرًا [...] أو يعرض قيمة يمكن أن تعمل التعليمات البرمجية الخاصة بك عليها - ولكنها ليست ما تم تمريره بواسطة المتصل ، وهو ما سيكون مفاجئًا بعض الشيء للمتصل

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

المشكلة هي أنه يمكن لأي شخص إنشاء نوع ليس في المجموع الذي يتم تجميعه ويمكن تمريره إلى الحزمة الخاصة بك.

بدون إضافة أنواع الجمع المناسبة للغة ، هناك ثلاثة خيارات للتعامل معها

  1. تجاهل الموقف
  2. التحقق من صحة والذعر / إرجاع خطأ
  3. حاول أن "تفعل ما تعنيه" من خلال استخراج القيمة المضمنة واستخدامها بشكل ضمني

3 تبدو مزيجًا غريبًا من 1 و 2 بالنسبة لي: لا أرى ما تشتريه.

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

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

هل أنا أسيء فهم القصد من النمط أم أننا نتعامل معه من فلسفات مختلفة؟

urandom سأكون ممتنًا أيضًا للتوضيح ؛ لست متأكدًا بنسبة 100٪ مما تحاول قوله أيضًا.

المشكلة هي أنه يمكن لأي شخص إنشاء نوع ليس في المجموع الذي يتم تجميعه ويمكن تمريره إلى الحزمة الخاصة بك.

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

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

أعتقد أن الأمر مختلف نوعًا: عندما يسيء الأشخاص استخدام التضمين بهذه الطريقة (على الأقل مع proto.Message والأنواع الملموسة التي تطبقه) ، فهم لا يفكرون عمومًا فيما إذا كان آمنًا وما هي الثوابت التي قد تنكسر . (يفترض المستخدمون أن الواجهات تصف السلوكيات المطلوبة تمامًا ، ولكن عندما يتم استخدام الواجهات كأنواع اتحاد أو مجموع ، فإنها غالبًا لا تفعل ذلك. راجع أيضًا https://github.com/golang/protobuf/issues/364.)

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

Merovius ربما لم أكن واضحًا: حقيقة أن المترجم سيخبر شخصًا ما باستخدام تضمين خطأ هو أكثر من فائدة جانبية جيدة.

أكبر مكسب لميزة الأمان هو أنه سيتم تكريمها من خلال الانعكاس وتمثيلها في go / type. هذا يعطي الأدوات والمكتبات مزيدًا من المعلومات للعمل معها. هناك العديد من الطرق لمحاكاة أنواع المجموع في Go ولكنها جميعها متطابقة مع رمز نوع غير مجموع ، لذا تحتاج الأدوات والمكتبة إلى معلومات خارج النطاق لتعرف أنها نوع مجموع ويجب أن تكون قادرة على التعرف على النمط المحدد يتم استخدامها ولكن حتى تلك الأنماط تسمح باختلاف كبير.

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

فائدة جانبية أخرى للأمان تعني أن المترجم لديه المزيد من المعلومات ويمكنه إنشاء كود أسرع بشكل أفضل.

هناك أيضًا حقيقة أنه بالإضافة إلى القدرة على استبدال المجموع الزائف بواجهات ، يمكنك استبدال المجموع الزائف "أحد هذه الأنواع العادية" مثل json.Token أو driver.Value . هذه قليلة ومتباعدة ولكنها ستكون مكانًا أقل حيث يكون interface{} ضروريًا.

كما أنه يجعل الطريقة الوحيدة لإنشاء قيمة غير صالحة غير آمنة

لا أعتقد أنني أفهم تعريف "القيمة غير الصالحة" التي تؤدي إلى هذه العبارة.

neild إذا كان لديك

var v pick {
  None struct{}
  A struct { X int; Y *T}
  B int
}

سيتم وضعها في الذاكرة مثل

struct {
  activeField int //which of None (0), A (1), or B (2) is the current field
  theInt int // If None always 0
  thePtr *T // If None or B, always nil
}

ومع عدم الأمان ، يمكنك تعيين thePtr حتى إذا كان activeField 0 أو 2 أو تعيين قيمة theInt حتى لو كان activeField 0.

في كلتا الحالتين ، سيؤدي هذا إلى إبطال الافتراضات التي قد يتخذها المترجم ويسمح بنفس النوع من الأخطاء النظرية التي يمكن أن تكون لدينا اليوم.

ولكن كما أشار bcmills إذا كنت تستخدم مادة غير آمنة ، فمن الأفضل أن تعرف ما تفعله لأنه الخيار النووي.

ما لا أفهمه هو سبب كون عدم الأمان هو الطريقة الوحيدة لإنشاء قيمة غير صالحة.

var t time.Timer

t غير صالحة؛ t.C يتم ضبطه ، استدعاء t.Stop سيؤدي إلى الذعر ، وما إلى ذلك. لا يلزم وجود غير آمن.

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

neild نعم آسف لأنني

كان ينبغي أن أقول باطل فيما يتعلق بثوابت نوع المجموع .

يمكن بالطبع أن تكون الأنواع الفردية في المجموع في حالة غير صالحة.

ومع ذلك ، فإن الحفاظ على ثوابت نوع المجموع يعني أنه يمكن الوصول إليها من أجل التفكير والذهاب / الأنواع بالإضافة إلى المبرمج ، لذا فإن التلاعب بها في المكتبات والأدوات يحافظ على هذه السلامة ويوفر المزيد من المعلومات لمبرمج metaprogram

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

func F(sum SumInterface) {
    switch v := sum {
    case Screwdriver:
             ...
    default:
           panic ("Someone implementing a new type which gets passed to F and causes a runtime panic 3 weeks into production")
    }
}

لذلك ، يبدو لي أن معظم المشكلات التي يواجهها الأشخاص مع محاكاة نوع المجموع القائمة على الواجهة يمكن حلها عن طريق التعرفة و / أو الاتفاقية. على سبيل المثال ، إذا كانت الواجهة تحتوي على طريقة غير مُصدرة ، فسيكون من التافه معرفة جميع عمليات التنفيذ الممكنة (نعم ، التحايل المتعمد). وبالمثل ، لمعالجة معظم المشكلات المتعلقة بالتعدادات المستندة إلى ذرات ، فإن اصطلاحًا بسيطًا "التعداد هو type Foo int مع إعلان بالصيغة const ( FooA Foo = iota; FooB; FooC ) " من شأنه تمكين كتابة أدوات شاملة ودقيقة لهم ايضا.

نعم ، هذا لا يعادل أنواع المجموع الفعلي (من بين أشياء أخرى ، لن يحصلوا على دعم انعكاس من الدرجة الأولى ، على الرغم من أنني لا أفهم حقًا مدى أهمية ذلك على أي حال) ، لكن هذا يعني أن الحلول الحالية تظهر ، من وجهة نظري ، أفضل مما يتم رسمها في كثير من الأحيان. و IMO سيكون من المفيد استكشاف مساحة التصميم هذه قبل وضعها فعليًا في Go 2 - على الأقل إذا كانت مهمة جدًا للناس.

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

ميروفيوس هذا وضع جيد.

سيسمح دعم الانعكاس للمكتبات وكذلك الأدوات غير المتصلة بالإنترنت - linters ، ومولدات الشفرات ، وما إلى ذلك - بالوصول إلى المعلومات ومنعها من تعديلها بشكل غير لائق والذي لا يمكن اكتشافه بشكل ثابت بأي دقة.

بغض النظر ، إنها فكرة عادلة أن نستكشفها ، لذا دعنا نستكشفها.

لتلخيص أكثر عائلات الزائفة شيوعًا في Go هي: (تقريبًا بترتيب الحدوث)

  • تعداد / ذرة.
  • واجهة مع طريقة الوسم لمجموع أنواع محددة في نفس الحزمة.
  • *T مقابل T اختياري
  • بنية مع تعداد تحدد قيمته الحقول التي يمكن تعيينها (عندما يكون التعداد منطقيًا ولا يوجد سوى حقل واحد آخر ، فهذا نوع آخر من T اختياري)
  • interface{} يقتصر على حقيبة صغيرة من مجموعة محدودة من الأنواع.

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

سيكون من الصعب حقًا التمييز بين *T مقابل T اختياريًا عن المؤشر العادي. يمكن إعطاء هذا الاصطلاح type O = *T . سيكون من الممكن اكتشاف ذلك ، على الرغم من صعوبة بعض الشيء نظرًا لأن الاسم المستعار ليس جزءًا من النوع. سيكون اكتشاف type O *T أسهل ولكن من الصعب العمل باستخدام الكود. من ناحية أخرى ، فإن كل ما يجب القيام به مدمج بشكل أساسي في النوع ، لذلك هناك القليل الذي يمكن اكتسابه في الأدوات من التعرف على هذا. دعنا فقط نتجاهل هذا. (من المحتمل أن تسمح العوامل العامة بشيء على غرار type Optional(T) *T والذي من شأنه تبسيط "وضع العلامات" عليها).

سيكون من الصعب التفكير في البنية ذات التعداد في الأدوات ، ما هي الحقول التي تتوافق مع أي قيمة للعداد؟ يمكننا تبسيط هذا إلى اصطلاح أنه يجب أن يكون هناك حقل واحد لكل عضو في التعداد وأن قيمة التعداد وقيمة الحقل يجب أن تكون متطابقة ، على سبيل المثال:

type Which int
const (
  A Which = iota
  B
  C
)
type Sum struct {
  Which
  A struct{} // has to be included to line up with the value of Which
  B struct { X int; Y float64 }
  C struct { X int; Y int } 
}

لن يتم الحصول على أنواع اختيارية ولكن يمكننا حالة خاصة "حقلين ، أولاً منطقي" في أداة التعرّف.

سيكون من المستحيل اكتشاف استخدام interface{} لمبلغ الحقيبة بدون تعليق سحري مثل //gosum: int, float64, string, Foo

بالتناوب ، يمكن أن تكون هناك حزمة خاصة بالتعريفات التالية:

package sum
type (
  Type struct{}
  Enum int
  OneOf interface{}
)

ولا تتعرف على التعدادات إلا إذا كانت على شكل type MyEnum sum.Enum ، لا تتعرف إلا على الواجهات والبنى فقط إذا قامت بتضمين sum.Type ، ولم تتعرف إلا على حقائب الاستيلاء على interface{} مثل type GrabBag sum.OneOf (لكن هذا سيظل بحاجة إلى تعليق يمكن التعرف عليه من الجهاز لشرح تعليقاته). سيكون لذلك الإيجابيات والسلبيات التالية:
الايجابيات

  • صريح في الكود: إذا تم تمييزه على هذا النحو ، فهو نوع إجمالي بنسبة 100٪ ، ولا توجد نتائج إيجابية خاطئة.
  • يمكن أن تحتوي هذه التعريفات على وثائق توضح ما تعنيه ويمكن أن ترتبط وثائق الحزمة بالأدوات التي يمكن استخدامها مع هذه الأنواع
  • البعض قد يكون بعض الرؤية في التفكير
    سلبيات
  • الكثير من السلبيات الكاذبة من الكود القديم و stdlib (التي لن تستخدمها).
  • يجب استخدامها لتكون مفيدة ، لذا سيكون التبني بطيئًا ومن المحتمل ألا يصل أبدًا إلى 100٪ ، وستكون فعالية الأدوات التي تعرفت على هذه الحزمة الخاصة دالة على التبني ، لذا فهي مثيرة للاهتمام على الرغم من التجربة ولكنها غير واقعية على الأرجح.

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

يمكننا تجميع الأدوات تقريبًا في توليفة (مثل سترينجر) واستبطانية (مثل golint).

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

في جميع الحالات ، سيكون من الممكن إنشاء دالة تتحقق من صحة "واحد من" الثابت.

بالنسبة للتعدادات ، يمكن أن يكون هناك المزيد من الأدوات مثل سترينجر. في https://github.com/golang/go/issues/19814#issuecomment -291002852 ذكرت بعض الاحتمالات.

أكبر أداة توليد هي المترجم الذي يمكن أن ينتج كود آلة أفضل بهذه المعلومات ، ولكن حسنًا.

لا أستطيع التفكير في أي شخص آخر في الوقت الحالي. هل يوجد أي شيء في قائمة رغبات أي شخص؟

بالنسبة إلى الاستبطان ، فإن المرشح الواضح هو الفحص الشامل. بدون دعم اللغة ، هناك نوعان مختلفان من الفحص المطلوب

  1. التأكد من التعامل مع جميع الحالات الممكنة
  2. التأكد من عدم إنشاء حالات غير صالحة (مما قد يبطل العمل الذي قام به 1)

1 تافه ، لكنه سيتطلب جميع الحالات الممكنة وحالة افتراضية لأنه لا يمكن التحقق من 2 بنسبة 100٪ (حتى تجاهل غير آمن) ولا يمكنك توقع كل التعليمات البرمجية باستخدام الكود الخاص بك يعمل هذا linter على أي حال.

2 لا يمكن أن تتبع القيم حقًا من خلال عكس أو تحديد كل الكود الذي يمكن أن يولد حالة غير صالحة للمبلغ ، لكنه يمكن أن يكتشف الكثير من الأخطاء البسيطة ، مثل إذا قمت بتضمين نوع المجموع ثم استدعاء func به ، فيمكن أن يقول "لقد كتبت pkg.F (v) لكنك تقصد pkg.F (v.EmbeddedField)" أو "لقد مررت 2 إلى pkg.F ، استخدم pkg.B". بالنسبة للبنية ، لا يمكنها فعل الكثير لفرض الثابت أن حقل واحد يتم تعيينه في كل مرة باستثناء الحالات الواضحة حقًا مثل "أنت تقوم بتشغيل أي وفي الحالة X ، تقوم بتعيين الحقل F على قيمة غير صفرية ". يمكن أن يصر على استخدام وظيفة التحقق من الصحة التي تم إنشاؤها عند قبول القيم من خارج الحزمة.

الشيء الكبير الآخر هو أن يظهر في جودوك. يقوم godoc بالفعل بتجميع const / iota و # 20131 من شأنه أن يساعد في استخدام المجاميع الزائفة للواجهة. ليس هناك حقًا أي شيء يتعلق بإصدار الهيكل غير الصريح في التعريف بخلاف تحديد الثابت.

بالإضافة إلى الأدوات غير المتصلة بالإنترنت - أدوات الطباعة ، ومولدات الأكواد ، وما إلى ذلك.

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

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

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

لذا ، كان سؤالي عن قصد: ما هي حالة الاستخدام ، لوجود هذه المعلومات في وقت التشغيل؟

بغض النظر ، إنها فكرة عادلة أن نستكشفها ، لذا دعنا نستكشفها.

عندما قلت "استكشفها" ، لم أقصد "تعدادها وتجادل بشأنها في فراغ" ، كنت أعني "تنفيذ الأدوات التي تستخدم هذه الاتفاقيات ومعرفة مدى فائدتها / ضرورتها / عمليتها".

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

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

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

FWIW ، إذا كنت أتجاهل الحالات المعقدة ببساطة وأركز على الأشياء التي تعمل: أ) الأساليب غير المُصدرة في الواجهات و ب) تعدادات الثبات البسيطة ، والتي لها int كنوع أساسي وثابت واحد- إعلان التنسيق المتوقع. قد يتطلب استخدام الأداة استخدام أحد هذين الحلين ، لكن IMO لا بأس به (لاستخدام أداة المترجم ، ستحتاج أيضًا إلى استخدام المجاميع بشكل صريح ، لذلك يبدو ذلك جيدًا).

هذا بالتأكيد مكان جيد للبدء ويمكن الاتصال به بعد تشغيله على مجموعة كبيرة من الحزم ومعرفة عدد الإيجابيات / السلبيات الخاطئة الموجودة

https://godoc.org/github.com/jimmyfrasche/closed

لا يزال هناك الكثير من العمل الجاري. لا أستطيع أن أعدك أنني لن أضطر إلى إضافة معلمات إضافية للمُنشئ. ربما يحتوي على أخطاء أكثر من الاختبارات. لكنها جيدة بما يكفي للعب بها.

يوجد مثال على الاستخدام في cmds / shut-exporer والذي سيدرج أيضًا جميع الأنواع المغلقة المكتشفة في الحزمة المحددة بواسطة مسار الاستيراد الخاص بها.

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

باستخدام التعدادات ، ذهبت في الاتجاه الآخر وسجلت للتو كل مجموعة ثابتة من نوع محدد. أخطط لفضح البتات المكتشفة أيضًا.

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

لقد بدأت للتو في اكتشاف جميع الواجهات بأساليب غير مُصدرة ولكنها شائعة إلى حدٍ ما ، وبينما كان بعضها من أنواع التلخيص الواضح ، لم يكن البعض الآخر كذلك.

سأجد أنه من المفيد إذا كان بإمكانك تقديم بعض الأمثلة التي لم تكن كذلك.

@ ميروفيوس آسف لم أحتفظ بقائمة. لقد وجدتها عن طريق تشغيل stdlib.sh (في cmds / Closed-explorer). إذا صادفت مثالًا جيدًا في المرة القادمة التي ألعب فيها بهذا ، فسأنشره.

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

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

أود أن أضع كلمة جيدة لاستكشاف كيف يمكن لأنواع الاتحاد أن تقلل من استخدام الذاكرة. أنا أكتب مترجمًا فوريًا في Go ولدي نوع قيمة يتم تنفيذه بالضرورة كواجهة لأن القيم يمكن أن تكون مؤشرات لأنواع مختلفة. من المفترض أن يعني هذا أن القيمة [] تشغل ضعف مساحة الذاكرة مقارنة بتعبئة المؤشر بعلامة بت صغيرة كما يمكنك أن تفعل في C. يبدو الأمر كثيرًا؟

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

لم أقم بأي اختبار أداء ؛ مجرد الإشارة إلى اتجاه البحث.

يمكنك تطبيق القيمة على أنها غير آمنة ، بدلاً من ذلك ، المؤشر.

في 6 فبراير 2018 ، الساعة 3:54 مساءً ، كتب "Brian Slesinsky" [email protected] :

أود أن أضع كلمة جيدة لاستكشاف كيف يمكن أن تقلل أنواع الاتحاد
استخدام الذاكرة. أنا أكتب مترجمًا فوريًا في Go ولدي نوع القيمة
يتم تطبيقه بالضرورة كواجهة لأن القيم يمكن أن تكون مؤشرات
لأنواع مختلفة. من المفترض أن يعني هذا أن القيمة [] تأخذ ضعف ذلك
مقارنة بتعبئة المؤشر بعلامة صغيرة كما يمكنك القيام به
في C. يبدو كثيرًا؟

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

لم أقم بأي اختبار أداء ؛ مجرد الإشارة إلى اتجاه
ابحاث.

-
أنت تتلقى هذا لأنه تم ذكرك.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/19412#issuecomment-363561070 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AGGWBz-L3t0YosVIJmYNyf2iQ-YgIXLGks5tSLv9gaJpZM4MTmSr
.

skybrian يبدو ذلك مغرورًا جدًا فيما يتعلق بتنفيذ أنواع المجموع. لا يتطلب الأمر فقط أنواع الجمع ، ولكن أيضًا أن يتعرف المترجم على الحالة الخاصة للمؤشرات فقط في مجموع ويحسنها كمؤشر معبأ - ويتطلب من GC أن يكون على دراية بعدد بتات العلامات المطلوبة في المؤشر ، لإخفائهم. مثل ، لا أرى حقًا حدوث هذه الأشياء ، TBH.

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

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

DemiMarie unsafe.Pointer لا يعمل على App Engine ، وعلى أي حال ، لن يسمح لك بتعبئة البتات دون العبث بمجمع القمامة. حتى لو كان ذلك ممكنًا ، فلن يكون قابلاً للنقل.

Merovius نعم ، يتطلب الأمر تغيير وقت التشغيل ومجمع البيانات المهملة لفهم تخطيطات الذاكرة المعبأة. هذا نوع من النقطة. تتم إدارة المؤشرات بواسطة وقت تشغيل Go ، لذلك إذا كنت تريد أن تعمل بشكل أفضل من الواجهات بطريقة آمنة ، فلا يمكنك القيام بذلك في مكتبة أو في المترجم.

لكنني سأعترف بسهولة أن كتابة مترجم فوري سريع هو حالة استخدام غير شائعة. ربما هناك آخرون؟ يبدو أن طريقة جيدة لتحفيز ميزة اللغة هي العثور على أشياء لا يمكن القيام بها بسهولة في Go today.

هذا صحيح.

فكرتي هي أن Go ليست أفضل لغة تكتب فيها مترجمًا ،
نظرًا للديناميكية الشديدة لمثل هذه البرامج. إذا كنت بحاجة إلى أداء عالٍ ،
يجب كتابة حلقاتك الساخنة في التجميع. هل هناك سبب لك
هل تحتاج إلى كتابة مترجم فوري يعمل على App Engine؟

في 6 فبراير 2018 6:15 مساءً ، كتب "Brian Slesinsky" [email protected] :

DemiMarie https://github.com/demimarie unsafe.Pointer لا يعمل على التطبيق
المحرك ، وعلى أي حال ، لن يسمح لك بتعبئة البتات بدونها
العبث بجامع القمامة. حتى لو كان ذلك ممكنًا ، فلن يكون كذلك
محمول.

metrovius نعم ، إنها تتطلب تغيير وقت التشغيل وجامع القمامة
لفهم تخطيطات الذاكرة المكدسة. هذا نوع من النقطة. المؤشرات
تدار بواسطة وقت تشغيل Go ، لذلك إذا كنت تريد أن تفعل ما هو أفضل من الواجهات في ملف
بطريقة آمنة ، لا يمكنك القيام بذلك في مكتبة أو في المترجم.

لكنني سأعترف بسهولة أن كتابة مترجم فوري سريع هو استخدام غير شائع
قضية. ربما هناك آخرون؟ يبدو أنه طريقة جيدة لتحفيز أ
تتمثل ميزة اللغة في العثور على الأشياء التي لا يمكن القيام بها بسهولة في Go today.

-
أنت تتلقى هذا لأنه تم ذكرك.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/19412#issuecomment-363598572 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AGGWB65jRKg_qVPWTiq8LbGk3YM1RUasks5tSN0tgaJpZM4MTmSr
.

أجد اقتراح rogpeppe جذابًا للغاية. أتساءل أيضًا عما إذا كانت هناك إمكانية لإلغاء تأمين مزايا إضافية لتتماشى مع تلك التي تم تحديدها بالفعل بواسطةgriesemer.

يقول الاقتراح: "مجموعة الطريقة لنوع المجموع تحمل تقاطع مجموعة الطريقة
بجميع أنواع مكوناته ، باستثناء أي طرق لها نفس الشيء
اسم ولكن توقيعات مختلفة. ".

لكن النوع هو أكثر من مجرد مجموعة طرق. ماذا لو دعم نوع المجموع تقاطع العمليات التي تدعمها أنواع مكوناتها؟

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

var x int|float64

الفكرة هي أن ما يلي سوف يعمل.

x += 5

سيكون مكافئًا لكتابة مفتاح النوع الكامل:

switch i := x.(type) {
case int:
    x = i + 5
case float64:
    x = i + 5
}

يتضمن متغير آخر مفتاح نوع حيث يكون نوع المكون هو نفسه نوع مجموع.

type Num int | float64
type StringOrNum string | Num 
var x StringOrNum

switch i := x.(type) {
case string:
    // Do string stuff.
case Num:
    // Would be nice if we could use i as a Num here.
}

أيضًا ، أعتقد أن هناك تآزرًا رائعًا حقًا بين أنواع الجمع ونظام الأدوية الجنسية الذي يستخدم قيود النوع.

var x int|float64

ماذا عن var x, y int | float64 ؟ ما هي القواعد هنا عند إضافة هذه؟ ما هو التحويل الخاسر الذي يتم إجراؤه (ولماذا)؟ ماذا سيكون نوع النتيجة؟

لا يقوم Go بإجراء تحويلات تلقائية في التعبيرات (مثل C) عن قصد - فهذه الأسئلة ليس من السهل الإجابة عليها وهذا يؤدي إلى أخطاء.

ولمزيد من المتعة:

var x, y, z int|string|rune
x = 42
y = 'a'
z = "b"
fmt.Println(x + y + z)
fmt.Println(x + z + y)
fmt.Println(y + x + z)
fmt.Println(y + z + x)
fmt.Println(z + x + y)
fmt.Println(z + y + x)

كل من int و string و rune لها عامل تشغيل + ؛ ما هي الطباعة أعلاه ، ولماذا والأهم من ذلك كله ، كيف يمكن ألا تكون النتيجة محيرة تمامًا؟

ماذا عن var x, y int | float64 ؟ ما هي القواعد هنا عند إضافة هذه؟ ما هو التحويل الخاسر الذي يتم إجراؤه (ولماذا)؟ ماذا سيكون نوع النتيجة؟

Merovius لا يتم ضائع بشكل ضمني ، على الرغم من أنني أستطيع أن أرى كيف يمكن أن تعطي x + y البسيط لأنه يتضمن تحويلًا ضمنيًا محتملاً. لكن أيًا مما يلي سيتم تجميعه:

z = int(x) + int(y)
z = float64(x) + float64(y)

وبالمثل ، فإن مثال xyz الخاص بك لن يتم تجميعه لأنه يتطلب تحويلات ضمنية محتملة.

أعتقد أن عبارة "دعم تقاطع العمليات المدعومة" تبدو جيدة ولكنها لا تنقل تمامًا ما كنت أنوي. تساعد إضافة شيء مثل "التجميعات لجميع أنواع المكونات" في وصف الطريقة التي أعتقد أنها يمكن أن تعمل.

مثال آخر هو إذا كانت جميع أنواع المكونات عبارة عن شرائح وخرائط. سيكون من الجيد أن تكون قادرًا على الاتصال بـ len على نوع المجموع دون الحاجة إلى تبديل النوع.

كل int و string و rune لها عامل + ؛ ما هي الطباعة أعلاه ، ولماذا والأهم من ذلك كله ، كيف يمكن ألا تكون النتيجة محيرة تمامًا؟

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

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

مثال آخر هو المقارنة مع لا شيء:

var x []int | []string
fmt.Println(x == nil)  // Prints true
x = []string(nil)
fmt.Println(x == nil)  // Still prints true

كلا النوعين من المكونات هما نوع واحد على الأقل يمكن مقارنته بـ لا شيء ، لذلك نسمح بمقارنة نوع المجموع مع لا شيء بدون مفتاح نوع. بالطبع يتعارض هذا إلى حد ما مع الطريقة التي تتصرف بها الواجهات حاليًا ، ولكن قد لا يكون هذا أمرًا سيئًا وفقًا لـ https://github.com/golang/go/issues/22729

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

تكمن المشكلة في أن النتيجة ستكون إما أ) لها نفس المشكلات التي تواجهها التحويلات التلقائية أو ب) ستكون محدودة للغاية (ومربكة IMO) في نطاقها - أي أن جميع المشغلين سيعملون فقط مع حرفية غير مطبوعة ، في أحسن الأحوال.

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

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

الآن الأنواع الوحيدة التي يمكنك إضافتها مع الحفاظ على التوافق العكسي هي الأنواع التي تسمح بجميع عمليات الأنواع المكونة لها.

أوه ولكي أكون صريحًا بشأن هذا أيضًا: إنه يعني أنه لا يمكنك

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

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

سيظل سلوك التعيين كما هو موصوف بواسطة rogpeppe ولكن بشكل عام لست متأكدًا من فهمي لهذه النقطة.

إذا لم يكن هناك شيء آخر ، أعتقد أن اقتراح rogpeppe الأصلي يحتاج إلى توضيح فيما يتعلق بسلوك نوع المجموع خارج مفتاح النوع. مجموعة المهام والطريقة مغطاة ، ولكن هذا كل شيء. ماذا عن المساواة؟ أعتقد أنه يمكننا القيام بما هو أفضل مما تفعله الواجهة {}:

var x int | float64
fmt.Println(x == "hello")  // compilation error?
x = 0.0
fmt.Println(x == 0) // true or false?  I vote true :-)

لذلك إذا قمت بإضافة نوع مكون جديد لا يقوم بتنفيذ مجموعة الطريقة هذه ، فسيكون هذا تغييرًا غير متوافق مع الإصدارات السابقة.

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

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

سيظل سلوك التعيين كما هو موضح بواسطةrogpeppe

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

fmt.Println(x == "hello") // compilation error?

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

يمكن مقارنة القيمة x للنوع غير السطحي X وقيمة t لنوع الواجهة T عندما تكون قيم النوع X قابلة للمقارنة وتنفذ X T. فهي متساوية إذا كان النوع الديناميكي لـ t مطابقًا لـ X والقيمة الديناميكية لـ t تساوي x .

fmt.Println(x == 0) // true or false? I vote true :-)

يفترض خطأ. بالنظر إلى أن ما شابه ذلك

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)

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

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

لكن أكرر: أنا لا أجادل لصالح اقتراح مختلف للمبالغ. أنا أجادل ضد هذا.

fmt.Println(x == "hello") // compilation error?

من المحتمل أن يضاف هذا إلى اقتراحهم أيضًا.

تصحيح: المواصفات تغطي بالفعل خطأ التجميع هذا ، نظرًا لاحتوائه على البيان

في أي مقارنة ، يجب أن يكون المعامل الأول قابلاً للتخصيص لنوع المعامل الثاني ، أو العكس.

Merovius ، لقد

fmt.Println(x == 0) // true or false? I vote true :-)

يفترض خطأ. بالنظر إلى أن ما شابه ذلك

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)
يجب أن يكون خطأ تجميعي (كما استنتجنا أعلاه) ،

لا أجد هذا المثال مقنعًا للغاية لأنك إذا قمت بتغيير السطر الأول إلى var x float64 = 0.0 فيمكنك استخدام نفس المنطق لتوضيح أن مقارنة float64 بـ 0 يجب أن تكون خاطئة. (نقاط ثانوية: (أ) أفترض أنك كنت تقصد float64 (0) في السطر الأول ، نظرًا لأن 0.0 قابلة للتخصيص إلى int. (ب) x == y لا ينبغي أن يكون خطأ تجميع في المثال الخاص بك. يجب أن تطبع خطأ بالرغم من ذلك.)

أعتقد أن فكرتك القائلة بأن "هذا مشابه لتعيين ثابت لنوع واجهة وبالتالي يجب أن يكون له نوعه الافتراضي" أكثر إقناعًا (بافتراض أنك تقصد نوع الجمع) ، لذلك سيكون المثال:

var x,y int|float64 = float64(0), 0
fmt.Println(x == y) // خطأ

ما زلت أجادل بأن x == 0 يجب أن يكون صحيحًا بالرغم من ذلك. نموذجي العقلي هو أن النوع يُعطى للصفر في وقت متأخر قدر الإمكان. أدرك أن هذا يتعارض مع السلوك الحالي للواجهات وهذا بالضبط سبب طرحه. أوافق على أن هذا لم يؤد إلى "الكثير من الغموض" ، ولكن المشكلة المماثلة المتمثلة في مقارنة الواجهات بالعدم أدت إلى الكثير من الالتباس. أعتقد أننا سنرى قدرًا مشابهًا من الارتباك عند المقارنة بـ 0 إذا ظهرت أنواع المجموع وتم الاحتفاظ بدلالات المساواة القديمة.

لا أجد هذا المثال مقنعًا للغاية لأنك إذا قمت بتغيير السطر الأول إلى var x float64 = 0.0 ، فيمكنك استخدام نفس المنطق لتوضيح أن مقارنة float64 بـ 0 يجب أن تكون خاطئة.

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

لاحظ أن مقارنة float64(0) بـ int(0) (على سبيل المثال المثال بالمجموع الذي تم استبداله بـ var x float64 = 0.0 ) ليس false ، على الرغم من أنه وقت تجميع خطأ (كما ينبغي أن يكون). هذه بالضبط وجهة نظري . يكون اقتراحك مفيدًا حقًا فقط عند دمجه مع ثوابت غير نمطية ، لأنه لن يتم تجميعه لأي شيء آخر.

(أ) أفترض أنك كنت تقصد float64 (0) في السطر الأول ، حيث يمكن تخصيص 0.0 لـ int.

بالتأكيد (كنت أفترض أن الدلالات أقرب إلى "النوع الافتراضي" الحالي للتعبيرات الثابتة ، لكنني أوافق على أن الصياغة الحالية لا تعني ذلك).

(ب) x == y يجب ألا يكون خطأ تجميعي في مثالك. يجب أن تتم طباعة خطأ بالرغم من ذلك).

لا ، يجب أن يكون خطأ وقت التجميع. لقد قلت ، أن العملية e1 == y ، مع e1 عبارة عن تعبير من نوع الجمع ، يجب السماح به فقط إذا كان التعبير سيتم تجميعه مع أي اختيار لنوع المكون. بالنظر إلى أنه في المثال الخاص بي ، يحتوي x على النوع int|float64 و y نوع int وبالنظر إلى أن float64 و int غير قابلة للمقارنة ، هذا الشرط منتهك بشكل واضح.

لإنشاء هذا التجميع ، ستحتاج إلى إسقاط الشرط الذي يحتاج إلى استبدال أي تعبير مكتوب مكون من مكونات أيضًا ؛ عند هذه النقطة نكون في موقف يتوجب علينا فيه وضع قواعد لكيفية ترقية الأنواع أو تحويلها عند استخدامها في هذه التعبيرات (تعرف أيضًا باسم "فوضى C").

كان الإجماع السابق هو أن أنواع المجموع لا تضيف الكثير إلى أنواع الواجهة.

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

لكن القصة مع Rust تُظهر أن استخدام أنواع مجموع NPD ومعالجة الأخطاء تمامًا كما يفعلون في Haskell فكرة سيئة: هناك سير عمل حتمي طبيعي نموذجي ونهج Haskellish لا يتناسب معه بشكل جيد.

مثال

اعتبر iotuils.WriteFile -like دالة في الكود الزائف. سيبدو التدفق الحتمي هكذا

file = open(name, os.write)
if file is error
    return error("cannot open " + name + " writing: " + file.error)
if file.write(data) is error:
    return error("cannot write into " + name + " : " + file.error)
return ok

وكيف تبدو في Rust

match open(name, os.write)
    file
        match file.write(data, os.write)
            err
                return error("cannot open " + name + " writing: " + err)
            ok
                return ok
    err
        return error("cannot write into " + name + " : " + err)

إنه آمن ولكنه قبيح.

واقتراحي:

type result[T, Err] oneof {
    default T
    Error Err
}

وكيف سيبدو البرنامج ( result[void, string] = !void )

file := os.Open(name, ...)
if !file {
    return result.Error("cannot open " + name + " writing: " + file.Error)
}
if res := file.Write(data); !res {
    return result.Error("cannot write into " + name + " : " + res.Error)
}
return ok

هنا الفرع الافتراضي مجهول ويمكن الوصول إلى فرع الخطأ باستخدام .Error (بمجرد معرفة النتيجة تكون خطأ). بمجرد معرفة أن الملف تم فتحه بنجاح ، يمكن للمستخدم الوصول إليه عبر المتغير نفسه. في البداية ، إذا تأكدنا من أن file تم فتحه بنجاح أو الخروج منه بطريقة أخرى (وبالتالي فإن المزيد من العبارات تعرف أن الملف ليس خطأ).

كما ترى ، يحافظ هذا النهج على التدفق الضروري ويوفر أمانًا للنوع. يمكن التعامل مع NPD بطريقة مماثلة:

type Reference[T] oneof {
    default T
    nil
}
// Reference[T] = *T

التعامل مشابه للنتيجة

sirkon ، مثال Rust الخاص بك لا يقنعني بوجود أي خطأ في أنواع الجمع المباشرة مثل Rust. بدلاً من ذلك ، فإنه يشير إلى أن مطابقة الأنماط في أنواع المجموع قد تكون أكثر شبهاً بالانتقال باستخدام عبارات if . شيء مثل:

ferr := os.Open(name, ...)
if err(e) := ferr {           // conditional match and unpack, initializing e
  return fmt.Errorf("cannot open %v: %v", name, e)
}
ok(f) := ferr                  // unconditional match and unpack, initializing f
werr := f.Write(data)
...

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

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

تضمين التغريدة

إنها ليست في الواقع لمعظم استخدامات Go: خدمات ومرافق الشبكة التافهة. ولكن بمجرد أن يكبر النظام ، هناك فرصة جيدة لأن يكون مفيدًا.
[...]
أعني أن ضمانات نظام النوع Go ضعيفة جدًا بالنسبة لشيء أكثر تعقيدًا من خدمات الشبكة البدائية النموذجية.

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

يمكن التعامل مع NPD بطريقة مماثلة

أعتقد أن هذا يوضح حقًا أن أسلوبك لا يضيف في الواقع أي قيمة مهمة. كما أشرت ، فإنه يضيف ببساطة بناء جملة مختلفًا للإشارة. لكن لا شيء يمنع المبرمج من استخدام بناء الجملة على قيمة صفرية (والتي من المفترض أنها لا تزال تشعر بالذعر). على سبيل المثال ، كل برنامج صالح باستخدام *p صالح أيضًا باستخدام p.T (أم أنه p.default ؟ من الصعب تحديد فكرتك على وجه التحديد) والعكس صحيح.

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

تضمين التغريدة

وبالنظر إلى أن جزءًا كبيرًا من مطوريها يعملون في Google ، يجب أن تفترض أنهم يعرفون أفضل منك ،

طوبى للمؤمنين.

كما أشرت ، فإنه يضيف ببساطة بناء جملة مختلفًا للإشارة.

تكرارا

var written int64
...
res := os.Stdout.Write(data) // Write([]byte) -> Result[int64, string] ≈ !int64
written += res // Will not compile as res is a packed result type
if !res {
    // we are living on non-default res branch thus the only choice left is the default
    return Result.Error(...)
}
written += res // is OK

تضمين التغريدة

ferr := os.Open(...)

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

sirkon يبدو أنك لا تهتم كثيرًا بالتحدث مع الناس وجهاً لوجه. سأترك الأمر عند هذا الحد.

دعونا نحافظ على محادثاتنا حضارية ، ونتجنب التعليقات غير البناءة. يمكننا أن نختلف في الأمور ، لكننا نحافظ على خطاب محترم. https://golang.org/conduct.

وبالنظر إلى أن جزءًا كبيرًا من مطوريها يعملون في Google ، يجب أن تفترض أنهم يعرفون أفضل منك

أشك في أنه يمكنك تقديم هذا النوع من الجدل في Google.

hasufell هذا الرجل من ألمانيا حيث ليس لديهم شركات تكنولوجيا معلومات كبيرة مع مقابلات هراء لضخ غرور الشخص الذي يجري المقابلة وإدارة عملاقة ، ولهذا السبب هذه الكلمات.

sirkon نفس الشيء ينطبق عليك. Ad-hominem والحجج الاجتماعية ليست مفيدة. هذه أكثر من مجرد مشكلة مدونة قواعد السلوك. لقد رأيت هذا النوع من "الحجج الاجتماعية" يظهر بشكل متكرر عندما يتعلق الأمر باللغة الأساسية: مطورو المترجم يعرفون بشكل أفضل ، ومصممو اللغة يعرفون بشكل أفضل ، وأشخاص Google يعرفون بشكل أفضل.

لا ، لم يفعلوا. لا توجد سلطة فكرية. هناك سلطة اتخاذ القرار فقط. تخلص منه.

إخفاء بعض التعليقات لإعادة ضبط المحادثة (وشكرًا agnivade لمحاولة إعادتها إلى القضبان).

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

اسمحوا لي ، من فضلك ، أن أضيف 2 سنتي لهذه المناقشة:

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

أقترح أن نضيف شيئًا مثل بناء نوع _family_ إلى اللغة.

النحو

يمكن تعريف عائلة النوع مثل أي نوع آخر:

type theFamilyName family {
    someType
    anotherType
}

قد يكون بناء الجملة الرسمي شيئًا مثل:
FamilyType = "family" "{" { TypeName ";" } "}" .

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

func Display(s family{string; fmt.Stringer}) { /* function body */ }

أي أن تعريف السطر الواحد يتطلب فاصلة منقوطة بين أسماء الأنواع.

القيمة الصفرية لنوع العائلة هي لا شيء ، كما هو الحال مع واجهة لا شيء.

(تحت الغطاء ، يتم تنفيذ القيمة الموجودة خلف تجريد العائلة مثل الواجهة.)

المنطق

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

سيتيح الحل المقترح أمانًا أفضل للنوع ، ويتم فحصه بالكامل في وقت التجميع وعدم إضافة أي نفقات إضافية في وقت التشغيل.

النقطة المهمة هي أن _Go code يجب أن تكون أكثر توثيقًا ذاتيًا_. يجب أن يتم تضمين ما يمكن أن تتخذه الوظيفة كوسيطة في الكود نفسه.

يستغل الكثير من التعليمات البرمجية بشكل غير صحيح حقيقة أن "الواجهة {} لا تقول شيئًا". إنه أمر محرج قليلاً أن مثل هذا البناء المستخدم على نطاق واسع (وسوء الاستخدام) في Go ، والذي بدونه لن نكون قادرين على فعل الكثير ، كما يقول _لا شيء _.

بعض الأمثلة

تتضمن وثائق الوظيفة sql.Rows.Scan كتلة كبيرة توضح بالتفصيل الأنواع التي يمكن تمريرها إلى الوظيفة:

Scan converts columns read from the database into the following common Go types and special types provided by the sql package:
 *string
 *[]byte
 *int, *int8, *int16, *int32, *int64
 *uint, *uint8, *uint16, *uint32, *uint64
 *bool
 *float32, *float64
 *interface{}
 *RawBytes
 any type implementing Scanner (see Scanner docs)

وبالنسبة لوظيفة sql.Row.Scan ، تتضمن الوثائق الجملة "راجع الوثائق على Rows.Scan للحصول على التفاصيل." انظر الوثائق لبعض الوظائف الأخرى للحصول على التفاصيل؟ هذا ليس الذهاب مثل وفي هذه الحالة أن الجملة ليست صحيحة لأنه في الحقيقة Rows.Scan يمكن أن تأخذ *RawBytes قيمة ولكن Row.Scan لا يمكن.

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

عندما تقول المستندات الخاصة بوظيفة ما أن الوظيفة تعمل تمامًا مثل بعض الوظائف الأخرى - "لذا اذهب إلى الوثائق الخاصة بهذه الوظيفة الأخرى" - يمكنك تقريبًا ضمان إساءة استخدام الوظيفة في بعض الأحيان. أراهن أن معظم الناس ، مثلي ، اكتشفوا فقط أن *RawBytes غير مسموح به كوسيطة في Row.Scan فقط بعد الحصول على خطأ من Row.Scan ( يقول "sql: RawBytes غير مسموح به في Row.Scan"). من المحزن أن نظام الكتابة يسمح بمثل هذه الأخطاء.

يمكننا بدلاً من ذلك الحصول على:

type Value family {
    *string
    *[]byte
    *int; *int8; *int16; *int32; *int64
    *uint; *uint8; *uint16; *uint32; *uint64
    *bool
    *float32; *float64
    *interface{}
    *RawBytes
    Scanner
}

بهذه الطريقة ، يجب أن تكون القيمة التي يتم تمريرها أحد الأنواع في العائلة المحددة ، ولن يحتاج مفتاح النوع داخل الوظيفة Rows.Scan للتعامل مع أي حالات غير متوقعة أو افتراضية ؛ سيكون هناك عائلة أخرى لوظيفة Row.Scan .

ضع في اعتبارك أيضًا أن cloud.google.com/go/datastore.Property Struct يحتوي على حقل "قيمة" من النوع interface{} ويتطلب كل هذه الوثائق:

// Value is the property value. The valid types are:
// - int64
// - bool
// - string
// - float64
// - *Key
// - time.Time
// - GeoPoint
// - []byte (up to 1 megabyte in length)
// - *Entity (representing a nested struct)
// Value can also be:
// - []interface{} where each element is one of the above types
// This set is smaller than the set of valid struct field types that the
// datastore can load and save. A Value's type must be explicitly on
// the list above; it is not sufficient for the underlying type to be
// on that list. For example, a Value of "type myInt64 int64" is
// invalid. Smaller-width integers and floats are also invalid. Again,
// this is more restrictive than the set of valid struct field types.
//
// A Value will have an opaque type when loading entities from an index,
// such as via a projection query. Load entities into a struct instead
// of a PropertyLoadSaver when using a projection query.
//
// A Value may also be the nil interface value; this is equivalent to
// Python's None but not directly representable by a Go struct. Loading
// a nil-valued property into a struct will set that field to the zero
// value.

قد يكون هذا:

type PropertyVal family {
  int64
  bool
  string
  float64
  *Key
  time.Time
  GeoPoint
  []byte
  *Entity
  nil
  []int64; []bool; []string; []float64; []*Key; []time.Time; []GeoPoint; [][]byte; []*Entity
}

(يمكنك أن تتخيل كيف يمكن تقسيم المنظف إلى عائلتين).

تم ذكر النوع json.Token أعلاه. تعريف النوع سيكون:

type Token family {
    Delim
    bool
    float64
    Number
    string
    nil
}

مثال آخر حصلت عليه مؤخرًا:
عند استدعاء وظائف مثل sql.DB.Exec ، أو sql.DB.Query ، أو أي دالة تأخذ قائمة متغيرة من interface{} حيث يجب أن يكون لكل عنصر نوع في مجموعة معينة و _ لا يكون هو نفسه a slice_ ، من المهم أن تتذكر استخدام عامل "السبريد" عند تمرير الوسيطات من []interface{} إلى مثل هذه الوظيفة: من الخطأ قول DB.Exec("some query with placeholders", emptyInterfaceSlice) ؛ الطريقة الصحيحة هي: DB.Exec("the query...", emptyInterfaceSlice...) حيث emptyInterfaceSlice اكتب []interface{} . الطريقة الأنيقة لجعل مثل هذه الأخطاء مستحيلة تتمثل في جعل هذه الوظيفة تأخذ وسيطة متغيرة Value ، حيث يتم تعريف Value كعائلة كما هو موضح أعلاه.

الهدف من هذه الأمثلة هو أنه يتم ارتكاب أخطاء حقيقية بسبب عدم دقة interface{} .

var x int | float64 | string | rune
z = int(x) + int(y)
z = float64(x) + float64(y)

يجب أن يكون هذا بالتأكيد خطأ في المترجم لأن نوع x غير متوافق حقًا مع ما يمكن تمريره إلى int() .

تعجبني فكرة الحصول على family . ستكون في الأساس واجهة مقيدة (مقيدة؟) للأنواع المدرجة ويمكن للمجمع أن يضمن أنك تتطابق مع كل الوقت ويغير نوع المتغير ضمن السياق المحلي المقابل لـ case .

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

هذا في الواقع هو السبب في أنني بدأت أكره بعض الأشياء مثل

func foo() (..., error) 

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

وبعض الأشياء الأخرى التي تعرض واجهة بدلاً من نوع محدد. بعض الوظائف
إرجاع net.Addr وأحيانًا يكون من الصعب بعض الشيء البحث في الكود المصدري لمعرفة نوع net.Addr الذي يتم إرجاعه بالفعل ثم استخدامه بشكل مناسب. ليس هناك الكثير من الجوانب السلبية في إعادة نوع ملموس (لأنه يطبق الواجهة ويمكن بالتالي استخدامه في أي مكان حيث يمكن استخدام الواجهة) إلا عندما
تخطط لاحقًا لتوسيع طريقتك لإرجاع نوع مختلف من net.Addr . ولكن إذا كان لديك
تشير API إلى أنها تُرجع OpError فلماذا لا تجعل هذا الجزء من مواصفات "Compile time"؟

على سبيل المثال:

 OpError is the error type usually returned by functions in the net package. It describes the operation, network type, and address of an error. 

عادة؟ لا يخبرك بالضبط ما هي الوظائف التي ترجع هذا الخطأ. وهذا هو توثيق النوع وليس الوظيفة. وثائق Read لا تذكر في أي مكان أنها ترجع OpError. أيضا ، إذا كنت تفعل

err := blabla.(*OpError)

سيتعطل بمجرد إرجاع نوع مختلف من الخطأ. لهذا السبب أود حقًا أن أرى هذا كجزء من إعلان الوظيفة. سيخبرك *OpError | error على الأقل أنه سيعود
مثل هذا الخطأ وسيحرص المترجم على عدم القيام بتأكيد نوع غير محدد يؤدي إلى تعطل برنامجك في المستقبل.

راجع للشغل: هل تم التفكير في نظام مثل تعدد الأشكال من نوع هاسكل؟ أو نظام نوع يعتمد على سمة مثل:

func calc(a < add(a, a) a >, b a) a {
   return add(a, b)
}

func drawWidgets(widgets []< widgets.draw() error >) error {
  for _, widgets := range widgets {
    err := widgets.draw()
    if err != nil {
      return err
    }
  }
  return nil
}

يعني a < add(a, a) a "مهما كان نوع a ، يجب أن توجد وظيفة add (typeof a، typeof a) typeof a)". يعني < widgets.draw() error> أنه "مهما كان نوع الأداة ، يجب أن توفر طريقة رسم ترجع خطأ". سيسمح هذا بإنشاء المزيد من الوظائف العامة:

func Sum(a []< add(a,a) a >) a {
  sum := a[0]
  for i := 1; i < len(a); i++ {
    sum = add(sum,a[i])
  }
  return sum
}

(لاحظ أن هذا لا يساوي "الأدوية" التقليدية).

ليس هناك الكثير من الجوانب السلبية في إعادة نوع ملموس (لأنه ينفذ الواجهة ويمكن بالتالي استخدامه في أي مكان حيث يمكن استخدام الواجهة) إلا عندما تخطط لاحقًا لتوسيع طريقتك لإرجاع نوع مختلف من net.Addr .

أيضًا ، لا يحتوي Go على تصنيف فرعي متغير ، لذلك لا يمكنك استخدام func() *FooError كـ func() error عند الحاجة. وهو أمر مهم بشكل خاص لإرضاء الواجهة. وأخيرًا ، هذا لا يتم تجميعه:

func Foo() (FooVal, FooError) {
    // ...
}

func Bar(f FooVal) (BarVal, BarError) {
    // ...
}

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

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

أيضًا ، لا يحتوي Go على تصنيف فرعي متغير ، لذلك لا يمكنك استخدام func () * FooError كخطأ func () عند الحاجة. وهو أمر مهم بشكل خاص لإرضاء الواجهة. وأخيرًا ، هذا لا يتم تجميعه:

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

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

لست على علم بأي لغة تسمح بذلك (حسنًا ، باستثناء esolangs) ولكن كل ما عليك فعله هو الاحتفاظ بـ "عالم من النوع" (والذي هو أساسًا خريطة variable -> type ) وإذا كنت - قم بتعيين المتغير الذي تقوم فقط بتحديث نوعه في "عالم النوع".

لا أعتقد أنك بحاجة إلى استدلال بنوع معقد للقيام بذلك ولكنك تحتاج إلى تتبع أنواع المتغيرات ولكني أفترض أنك بحاجة إلى القيام بذلك على أي حال لأن

var int i = 0;
i = "hi";

عليك بالتأكيد بطريقة ما أن تتذكر أي المتغيرات / التصريحات التي لها أنواعها ، وبالنسبة إلى i = "hi" تحتاج إلى إجراء "بحث عن النوع" على i للتحقق مما إذا كان يمكنك تعيين سلسلة إليه.

هل هناك مشكلات عملية تعقد تعيين func () *ConcreteError إلى func() error بخلاف مدقق النوع الذي لا يدعمه (مثل أسباب وقت التشغيل / أسباب التعليمات البرمجية المجمعة)؟ أعتقد أنه سيتعين عليك حاليًا لفها في وظيفة مثل هذا:

type MyFunc func() error

type A struct {
}

func (_ *A) Error() string { return "" }

func NewA() *A {
    return &A{}
}

func main() {
    var err error = &A{}
    fmt.Println(err.Error())
    var mf MyFunc = MyFunc(func() error { return NewA() }) // type checks fine
        //var mf MyFunc = MyFunc(NewA) // doesn't type check
    _ = mf
}

إذا كنت تواجه func (a, b) c ولكن تحصل على func (x, y) z كل ما عليك فعله هو التحقق مما إذا كان z قابل للتخصيص إلى ca ، b يجب تخصيصها إلى x ، y ) والتي على الأقل على مستوى النوع لا تتضمن استدلالًا من النوع المعقد (يتضمن فقط التحقق مما إذا كان النوع قابل للتخصيص / متوافق مع / مع نوع آخر). بالطبع ، ما إذا كان هذا يتسبب في حدوث مشكلات في وقت التشغيل / التجميع ... لا أعرف ولكن على الأقل بالنظر بدقة إلى مستوى النوع ، لا أرى سبب احتواء ذلك على استدلال بنوع معقد. يعرف مدقق النوع بالفعل ما إذا كان يمكن x لـ a وبالتالي فإنه يعرف بسهولة أيضًا ما إذا كان يمكن func () x لـ func () a . بالطبع ، قد تكون هناك أسباب عملية (التفكير في تمثيلات وقت التشغيل) لماذا لن يكون هذا ممكنًا بسهولة. (أظن أن هذا هو الجوهر الحقيقي هنا ، وليس التحقق من النوع الفعلي).

من الناحية النظرية ، يمكنك حل مشكلات وقت التشغيل (إذا كان هناك) من خلال وظائف الالتفاف تلقائيًا (كما هو الحال في المقتطف أعلاه) مع الجانب السلبي _ المحتمل الضخم _ الذي يؤدي إلى فشل مقارنات funcs مع funcs (نظرًا لأن func المغلف لن يكون مساويًا لـ func يلتف).

لست على علم بأي لغة تسمح بذلك (حسنًا ، باستثناء لغة esolangs)

ليس بالضبط ، لكنني أجادل أن ذلك لأن اللغات ذات أنظمة الكتابة القوية عادة ما تكون لغات وظيفية لا تستخدم المتغيرات حقًا (وبالتالي لا تحتاج حقًا إلى القدرة على إعادة استخدام المعرفات). FWIW ، أود أن أزعم أنه على سبيل المثال ، سيكون نظام نوع Haskell قادرًا على التعامل مع هذا الأمر جيدًا - على الأقل طالما أنك لا تستخدم أي خصائص أخرى FooError أو BarError ، يجب أن يكون أن تكون قادراً على استنتاج أن err من النوع error والتعامل معه. بالطبع ، مرة أخرى ، هذا افتراضي ، لأن هذا الموقف الدقيق لا ينتقل بسهولة إلى لغة وظيفية.

لكنني أفترض أنك بحاجة إلى القيام بذلك على أي حال لأن

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

هل هناك مشكلات عملية تعقد تعيين func () *ConcreteError إلى func() error بخلاف مدقق النوع الذي لا يدعمه (مثل أسباب وقت التشغيل / أسباب التعليمات البرمجية المجمعة)؟

هناك مشاكل عملية ، لكنني أعتقد أنه مقابل func ، من المحتمل أن تكون قابلة للحل (عن طريق إصدار رمز un / -wrapping ، على غرار كيفية عمل تمرير الواجهة). كتبت قليلاً عن التباين في Go وشرحت بعض المشكلات العملية التي أراها في الأسفل. لست مقتنعًا تمامًا أنه يستحق الإضافة. أي لست متأكدًا من أنه يحل المشكلات المهمة من تلقاء نفسه.

مع الجانب السلبي الضخم المحتمل أنه يفسد مقارنات funcs مع funcs (لأن func المغلف لن يكون مساويًا لـ func فإنه يلتف).

funcs غير قابلة للمقارنة.

على أي حال ، TBH ، كل هذا يبدو خارج الموضوع قليلاً عن هذه المشكلة :)

لمعلوماتك: لقد فعلت هذا للتو . إنه ليس لطيفًا ، لكنه بالتأكيد آمن من النوع. (يمكن فعل نفس الشيء لـ # 19814 FWIW)

لقد تأخرت قليلاً في الحفلة ، لكني أيضًا أود أن أشارككم مشاعري بعد 4 سنوات من الذهاب:

  • كانت العوائد متعددة القيم خطأً فادحًا.
  • واجهات لا يمكن أن تكون خطأ.
  • المؤشرات ليست مرادفات لكلمة "اختيارية" ، كان يجب استخدام النقابات التمييزية بدلاً من ذلك.
  • من المفترض أن يكون JSON unmarshaller قد قام بإرجاع خطأ إذا لم يتم تضمين الحقل المطلوب في مستند JSON.

في السنوات الأربع الماضية ، وجدت العديد من المشكلات المرتبطة به:

  • إرجاع البيانات المهملة في حالة حدوث خطأ.
  • فوضى في بناء الجملة (إرجاع القيم الصفرية في حالة حدوث خطأ).
  • إرجاع متعدد الأخطاء (واجهات برمجة التطبيقات المربكة ، من فضلك ، لا تفعل ذلك!).
  • الواجهات غير الصفرية التي تشير إلى مؤشرات تشير إلى nil (تربك الجحيم بين الأشخاص الذين يجعلون عبارة "Go is a easy language" تبدو وكأنها مزحة سيئة).
  • تؤدي حقول JSON غير المحددة إلى تعطل الخوادم (نعم!).
  • تؤدي المؤشرات المرتجعة التي لم يتم التحقق منها إلى تعطل الخوادم ، ومع ذلك لم يقم أي شخص بتوثيق المؤشر الذي تم إرجاعه يمثل اختياريًا (ربما من النوع) ، وبالتالي يمكن أن يكون nil (نعم!)

ومع ذلك ، تتطلب التغييرات المطلوبة لإصلاح كل هذه المشكلات إصدار Go 2.0.0 غير متوافق حقًا (وليس Go2) ، والذي لن يتحقق أبدًا ، على ما أفترض. على أي حال...

هذه هي الطريقة التي يجب أن تبدو بها معالجة الأخطاء:

// Divide returns either a float64 or an arbitrary error
func Divide(dividend, divisor float64) float64 | error {
  if dividend == 0 {
    return errors.New("dividend is zero")
  }
  if divisor == 0 {
    return errors.New("divisor is zero")
  }
  return dividend / divisor
}

func main() {
  // type-switch statements enforce completeness:
  switch v := Divide(1, 0).(type) {
  case float64:
    log.Print("1/0 = ", v)
  case error:
    log.Print("1/0 = error: ", v)
  }

  // type-assertions, however, do not:
  divisionResult := Divide(3, 1)
  if v, ok := divisionResult.(float64); ok {
    log.Print("3/1 = ", v)
  }
  if v, ok := divisionResult.(error); ok {
    log.Print("3/1 = error: ", v.Error())
  }
  // yet they don't allow asserting types not included in the union:
  if v, ok := divisionResult.(string); ok { // compile-time error!
    log.Print("3/1 = string: ", v)
  }
}

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

لقد رأيت في كثير من الأحيان أشخاصًا مرتبكون تمامًا حول واجهات _non-nil لقيم لا شيء _ : https://play.golang.org/p/JzigZ2Q6E6F. عادةً ما يتم الخلط بين الأشخاص عندما تشير واجهة error إلى مؤشر من نوع خطأ مخصص يشير إلى nil ، وهذا أحد الأسباب التي أعتقد أن جعل الواجهات لا يمكن أن تكون خطأً.

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

يجب استخدام النقابات التمييزية في الاختيارات (ربما أنواع) وتمرير مؤشرات nil إلى الواجهات كان يجب أن يؤدي إلى حالة من الذعر:

type CustomErr struct {}
func (err *CustomErr) Error() string { return "custom error" }

func CouldFail(foo int) error | nil {
  var err *customErr
  if foo > 10 {
    // you can't return a nil pointer as an interface value
    return err // this will panic!
  }
  // no error
  return nil
}

func main() {
  // assume no error
  if err, ok := CouldFail().(error); ok {
    log.Fatalf("it failed, Jim! %s", err)
  }
}

المؤشرات وأنواع ربما غير قابلة للتبديل. يعد استخدام المؤشرات للأنواع الاختيارية أمرًا سيئًا لأنه يؤدي إلى إرباك واجهات برمجة التطبيقات:

// P returns a pointer to T, but it's not clear whether or not the pointer
// will always reference a T instance. It might be an optional T,
// but the documentation usually doesn't tell you.
func P() *T {}

// O returns either a pointer to T or nothing, this implies (but still doesn't guarantee)
// that the pointer is always expected to not be nil, in any other case nil is returned.
func O() *T | nil {}

ثم هناك أيضًا JSON. لا يمكن أن يحدث هذا أبدًا مع النقابات لأن المترجم يجبرك على التحقق منها قبل الاستخدام . يجب أن يفشل unmarshaller JSON إذا لم يتم تضمين حقل مطلوب (بما في ذلك حقول نوع المؤشر) في مستند JSON:

type DataModel struct {
  // Optional needs to be type-checked before use
  // and is therefore allowed to no be included in the JSON document
  Optional string | nil `json:"optional,omitempty"`
  // Required won't ever be nil
  // If the JSON document doesn't include it then unmarshalling will return an error
  Required *T `json:"required"`
}

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

read = (s String) -> (Array<Byte> or Error) => match s {
  "A" then Error<NotFound>
  "B" then Error<AccessDenied>
  "C" then Error<MemoryLimitExceeded>
  else Array<Byte>("this is fine")
}

main = () -> ?Error => {
  // assume the result is a byte array
  // otherwise throw the error up the stack wrapped in a "type-assertion-failure" error
  r = read("D") as Array<Byte>
  log::print("data: %s", r)
}

أود أن أرى هذا يصبح حقيقة ذات يوم. لذلك دعونا نرى ما إذا كان بإمكاني المساعدة قليلاً:

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

من وجهة نظري ، ستكون هذه

  1. اسمح فقط بـ | نسخة
    <any pointer type> | nil
    حيث سيكون أي نوع من أنواع المؤشرات: المؤشرات والوظائف والقنوات والشرائح والخرائط (أنواع مؤشر Go)
  2. نمنع تعيين nil لنوع المؤشر المجرد. إذا كنت تريد تعيين لا شيء ، فيجب أن يكون النوع <pointer type> | nil . على سبيل المثال:
var n *int       = nil // Does not compile, wrong type
var n *int | nil = nil // Ok!

var set map[string] bool       = nil // Does not compile
var set map[string] bool | nil = nil // Ok!

var myFunc func(int) err       = nil // Nope!
var myFunc func(int) err | nil = nil // All right.

هذه هي الأفكار الرئيسية. فيما يلي الأفكار المشتقة من الأفكار الرئيسية:

  1. لا يمكنك التصريح عن متغير من نوع المؤشر المجرد وتركه غير مهيأ. إذا كنت تريد القيام بذلك ، فأنت بحاجة إلى إضافة النوع المميز | nil
var maybeAString *string       // Wrong: invalid initial value
var maybeAString *string | nil // Good
  1. يمكنك تعيين نوع مؤشر مكشوف لنوع مؤشر "لا شيء" ، ولكن ليس العكس:
var value int = 42
var barePointer *int = &value          // Valid
var nilablePointer *int | nil = &value // Valid

nilablePointer = barePointer // Valid
barePointer = nilablePointer // Invalid: Incompatible types
  1. الطريقة الوحيدة للحصول على قيمة من نوع مؤشر "لا شيء" هي عبر مفتاح النوع ، كما أشار آخرون. على سبيل المثال ، باتباع المثال أعلاه ، إذا كنا نريد حقًا تعيين قيمة nilablePointer إلى barePointer ، فسنحتاج إلى القيام بما يلي:
switch val := nilablePointer.(type) {
  case *int:
    barePointer = val // Yeah! Types are compatible now. It is imposible that "val = nil"
  case nil:
    // Do what you need to do when nilablePointer is nil
}

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

  • أ) عدم وجود أخطاء في المؤشر . حسنًا ، لم تكن 4 كلمات تعني هذا القدر. لهذا السبب أشعر بالحاجة إلى قول ذلك من وجهة نظر أخرى: برنامج No Go سوف _EVER_ خطأ nil pointer dereference مرة أخرى! 💥
  • ب) يمكنك تمرير المؤشرات لوظيفة المعلمات دون تداول "الأداء مقابل النية" .
    ما أعنيه بهذا هو أنه في بعض الأوقات أرغب في تمرير بنية إلى دالة ، وليس مؤشرًا إليها ، لأنني لا أريد أن أكون قلقًا بشأن هذه الوظيفة وأجبرها على التحقق من المعلمات . ومع ذلك ، عادةً ما ينتهي بي الأمر بتمرير مؤشر لتجنب التحميل الزائد.
  • ج) لا مزيد من الخرائط! نعم! سننتهي مع التناقض حول "الشرائح الصفرية الآمنة" و "الخرائط الصفرية غير الآمنة" (سيؤدي ذلك إلى الذعر إذا حاولت الكتابة إليهم). ستتم تهيئة الخريطة أو تكون من النوع map | nil ، وفي هذه الحالة ستحتاج إلى استخدام مفتاح نوع 😃

ولكن هناك أيضًا شيء آخر غير ملموس هنا يجلب الكثير من القيمة: مطور البرامج راحة البال . يمكنك العمل واللعب باستخدام المؤشرات والوظائف والقنوات والخرائط وما إلى ذلك مع الشعور بالاسترخاء الذي لا داعي للقلق بشأن عدم وجودهم. _ سأدفع ثمن هذا! _ 😂

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

تتمثل إحدى المشكلات في أنه حتى هذه النسخة البسيطة من الاقتراح غير متوافقة مع الإصدارات السابقة ، ولكن يمكن إصلاحها بسهولة عن طريق gofix : فقط استبدل جميع إقرارات نوع المؤشر بـ <pointer type> | nil .

ماذا تعتقد؟ آمل أن يلقي هذا بعض الضوء ويسرع من تضمين nil-safety في اللغة. يبدو أن هذه الطريقة (من خلال "النقابات المميّزة") هي الطريقة الأبسط والأكثر تعامدًا لتحقيق ذلك.

تضمين التغريدة

لا يمكنك التصريح عن متغير من نوع المؤشر المجرد وتركه غير مهيأ.

هذا هو جوهر المسألة. هذا ليس شيئًا يفعله Go - كل نوع له قيمة صفرية ، نقطة كاملة. وإلا فسيتعين عليك الإجابة عما يفعله ، على سبيل المثال ، make([]T, 100) ؟ الأشياء الأخرى التي ذكرتها (على سبيل المثال ، عدم وجود خرائط خوف عند الكتابة) هي نتيجة لهذه القاعدة الأساسية. (وبغض النظر عن ذلك ، لا أعتقد أنه من الصحيح حقًا أن نقول إن الشرائح الصفرية أكثر أمانًا من الخرائط - فالكتابة إلى شريحة صفرية سيثير الذعر تمامًا مثل الكتابة على خريطة صفرية).

بعبارة أخرى: اقتراحك ليس بهذه البساطة في الواقع ، لأنه ينحرف بشكل كبير عن قرار التصميم الأساسي في لغة Go.

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

يبدو الاقتراح أعلاه أشبه بنوع اختياري / غير اختياري من الأشياء كما هو الحال في Swift وغيرها. إنه رائع وكل شيء عدا:

  1. سيؤدي ذلك إلى كسر كل برنامج إلى حد كبير ولن يكون الإصلاح تافهاً بالنسبة لـ gofix. لا يمكنك فقط استبدال كل شيء بـ <pointer type> | nil حسب الاقتراح ، سيتطلب هذا تبديل النوع لفك القيمة.
  2. لكي يكون هذا في الواقع قابلاً للاستخدام ويمكن تحمله ، سيحتاج Go إلى المزيد من السكر النحوي حول هذه الاختيارات. خذ Swift ، على سبيل المثال. هناك العديد من الميزات في اللغة خصيصًا للعمل مع الاختيارات - الحارس ، والربط الاختياري ، والتسلسل الاختياري ، وعدم دمج الصفوف ، وما إلى ذلك.

فلماذا لا ترفض القيمة الصفرية في الحالات التي لا تكون مفيدة.

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

تغيير Go في هذا الصدد سيكون مفيدًا

لها فوائد ، لكن هذا لا يعني أنها مفيدة. كما أن لها أضرار. أي وزن أثقل يعتمد على التفضيل والمقايضة. اختار مصممو Go هذا.

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

🤔 آها! كنت أعلم أن هناك شيئًا واضحًا كنت أفتقده. دوه! كلمة "بسيط" لها معاني معقدة. حسنًا ، لا تتردد في إزالة الكلمة "البسيطة" من تعليقي السابق.

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

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

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

type S struct {
    n int
}
var s S 
s.n  // Fine

var s *S
s.n // runtime error

var f func(int)
f() // runtime error

لذا ، ماذا لو:

  • حدد قيمة صفرية مفيدة لكل نوع من أنواع المؤشرات
  • فقط قم بتهيئته في المرة الأولى التي يتم استخدامه فيها (التهيئة البطيئة).

أعتقد أن هذا تم اقتراحه في قضية أخرى ، لست متأكدًا. أنا أكتبه هنا فقط لأنه يعالج نقطة التراجع الرئيسية لهذا الاقتراح.

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

| نوع المؤشر | القيمة الصفرية | قيمة صفرية ديناميكية | التعليق |
| --- | --- | --- | --- |
| * ت | nil | جديد (T) |
| [] تي | nil | [] تي {} |
| خريطة [T] يو | nil | خريطة [T] U {} |
| func | nil | noop | لذا فإن القيمة الصفرية الديناميكية للدالة لا تفعل شيئًا وتُرجع قيمًا صفرية. إذا انتهت قائمة قيم الإرجاع في error ، فسيتم إرجاع خطأ افتراضي يقول أن الوظيفة "لا توجد عملية" |
| chan T | nil | جعل (تشان تي) |
| interface | nil | - | تطبيق افتراضي حيث يتم تهيئة جميع الطرق باستخدام الوظيفة noop الموضحة أعلاه |
| نقابة تمييزية | nil | قيمة صفرية ديناميكية من النوع الأول | |

الآن ، عند تهيئة هذه الأنواع ، ستكون nil ، كما هي الآن. الاختلاف في اللحظة التي يتم فيها الوصول إلى nil . في تلك اللحظة ، سيتم استخدام قيمة الصفر الديناميكي. بعض الأمثلة:

type S struct {
    n int
}
var s *S
if s == nil { // true. Nothing different happens here
...
}
s.n = 1       // At this moment the go runtime would check if it is nil, and if it is, 
              // do "s = new(S)". We could say the code would be replaced by:
/*
if s == nil {
    s = new(S)
}
s.n = 1
*/

// -------------
var pointers []*S = make([]*S, 100) // Everything as usual
for _,p := range pointers {
    p.n = 1 // This is translated to:
    /*
        if p == nil {
            p = new(S)
        }
        p.n = 1
    */
}

// ------------
type I interface {
    Add(string) (int, error)
}

var i I
n, err := i.Add("yup!") // This method returns 0, and the default error "Noop"
if err != nil { // This condition is true and the error is returned
    return err
}

ربما أفتقد تفاصيل التنفيذ والصعوبات المحتملة ، لكنني أردت التركيز على الفكرة أولاً.

العيب الرئيسي هو أننا نضيف فحص صفري إضافي في كل مرة تصل فيها إلى قيمة نوع المؤشر. لكني أود أن أقول:

  • إنها مقايضة جيدة للفوائد التي نحصل عليها. يحدث نفس الموقف مع عمليات التحقق المقيدة في عمليات الوصول إلى المصفوفة / الشرائح ونقبل دفع غرامة الأداء تلك مقابل السلامة التي يجلبها.
  • يمكن تجنب عمليات تدقيق الصفري بنفس طريقة عمليات التحقق من الصفائف: إذا تمت تهيئة نوع المؤشر في النطاق الحالي ، يمكن للمجمع معرفة ذلك وتجنب إضافة علامة الاختيار الصفرية.

مع هذا ، لدينا جميع المزايا الموضحة في التعليق السابق ، بالإضافة إلى أننا لسنا بحاجة إلى استخدام مفتاح نوع للوصول إلى القيمة (التي ستكون فقط للنقابات التي تم تمييزها) ، مع الحفاظ على كود go نظيفًا مثل هو عليه الآن.

ماذا تعتقد؟ نعتذر إذا تم مناقشة هذا بالفعل. أيضًا ، أدرك أن اقتراح التعليق هذا أكثر ارتباطًا بـ nil من النقابات التمييزية. قد أقوم بنقل هذا إلى مشكلة متعلقة بعدم وجود شيء ، ولكن ، كما قلت ، قمت بنشره هنا لأنه يحاول إصلاح المشكلة الرئيسية للنقابات التي تم تمييزها: القيم الصفرية المفيدة.

عودة إلى الأمر: لذلك يبدو أن السبب الرئيسي وراء كبح هذا هو القيمة الصفرية.

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

لذا ، ماذا لو:

  • حدد قيمة صفرية مفيدة لكل نوع من أنواع المؤشرات
  • فقط قم بتهيئته في المرة الأولى التي يتم استخدامه فيها (التهيئة البطيئة).

يفشل هذا إذا قمت بتمرير نوع المؤشر. على سبيل المثال

func F(p *T) {
    *p = 42 // same as if p == nil { p = new(T) } *p = 42
}

func G() {
    var p *T
    F(p)
    fmt.Println(p == nil) // Has to be true, as F can't modify p. But now F is silently misbehaving
}

هذا النقاش هو كل شيء ما عدا الجديد. هناك أسباب تجعل أنواع المراجع تتصرف بالطريقة التي تتصرف بها ، وليس الأمر أن مطوري Go لم يفكروا في الأمر :)

هذا هو جوهر المسألة. هذا ليس شيئًا يفعله Go - كل نوع له قيمة صفرية ، نقطة كاملة. وإلا فسيتعين عليك الإجابة على ما يفعله ، على سبيل المثال إجراء ([] T ، 100)؟

يجب عدم السماح بهذا (و new(T) ) إذا لم تكن قيمة T صفرية. يجب أن تفعل make([]T, 0, 100) ثم تستخدم append لملء الشريحة. يجب أن يكون إعادة النسخ الأكبر ( v[:0][:100] ) خطأ أيضًا. [10]T نوعًا مستحيلًا (ما لم تتم إضافة القدرة على تأكيد شريحة إلى مؤشر مصفوفة إلى اللغة). وستحتاج إلى طريقة لتمييز أنواع nilable الموجودة على أنها غير قابلة للصفاء من أجل الحفاظ على التوافق مع الإصدارات السابقة.

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

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

حسنًا ، أنا أفهم هذا. علينا فقط أن نرى وجهة نظر الآخرين (أنا لا أقول إنك لا تفعل ذلك ، أنا فقط أوضحت نقطة: غمزة :) حيث يرون أن هذا شيء قوي لكتابة برامجهم. هل يتناسب مع Go؟ يعتمد ذلك على كيفية تنفيذ الفكرة ودمجها في اللغة ، وهذا ما نحاول جميعًا القيام به في هذا الموضوع (على ما أعتقد)

يفشل هذا إذا قمت بتمرير نوع المؤشر. على سبيل المثال (...)

لا أفهم هذا تمامًا. لماذا هذا فشل؟ أنت تقوم فقط بتمرير قيمة إلى معلمة الوظيفة ، والتي تصادف أن تكون مؤشرًا nil . ثم تقوم بتعديل تلك القيمة داخل الدالة. من المتوقع ألا ترى هذه التأثيرات خارج الوظيفة. دعني أعلق على بعض الأمثلة:

// Augmenting your example with more comments:
func FCurrentGo(p *T) {
    // Here "p" is just a value, which happens to be a pointer type. Doing...
    *p = 42
    // ...without checking first for "nil" is the recipe for hiding a bug that will crash the entire program, 
    // which is exactly what is happening in current Go code bases

    // The correct code would be:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func FWithDynamicZero(p *T) {
    // Here, again, p is just a value of a pointer type. Doing...
    *p = 42
    // would allocate a new T and assign 42. It is true that this doesn't have any effect on the "outside
    // world", which could be considered "incorrect" because you expected the function to do that.
    // If you really want to be sure "p" is pointing to something valid in the "outside world", then
    // check that:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func main() {
    var p *T
    FCurrentGo(p) // This will crash the program
        FWithDynamicZero(p) // This won't have any effect on "p". This is expected because "p" is not pointing
                            // to anything. No crash here.
    fmt.Println(p == nil) // It is true, as expected
}

يحدث موقف مشابه مع طرق المستقبل غير المؤشر ، وهو أمر محير للقادمين الجدد للذهاب (ولكن بمجرد فهمك لذلك ، يصبح الأمر منطقيًا):

type Point struct {
    x, y int
}

func (p Point) SetXY(x, y int) {
    p.x = x
    p.y = y
}

func main() {
    p := Point{x: 1, y: 2}
    p.SetXY(24, 42)

    pointerToP := &Point{x: 1, y: 2}
    pointerToP.SetXY(24, 42)

    fmt.Println(p, pointerToP) // Will print "{1 2} &{1 2}", which could confuse at first
}

لذلك نحن بحاجة للاختيار بين:

  • أ) فشل مع حادث
  • ب) فشل مع عدم تعديل صامت للقيمة المشار إليها بمؤشر عند تمرير هذا المؤشر إلى دالة.

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

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

ملاحظة 2: في النهاية ، هذه الفكرة هي فقط لقيم "لا شيء" وليس لها علاقة بالنقابات التي تم التمييز ضدها. لذلك سوف أقوم بإنشاء قضية مختلفة لتجنب تلويث هذه المشكلة

حسنًا ، أنا أفهم هذا. علينا فقط أن نرى وجهة نظر الآخرين (أنا لا أقول إنك لا تفعل ذلك ، أنا فقط أوضح نقطة )

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

لماذا هذا فشل؟

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

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

على سبيل المثال ، إذا جاز التعبير ، فأنت تتعامل مع مشكلة مختلفة عن الاقتراح الفعلي للمؤشرات غير القابلة للصفقة: بالنسبة لهذا الاقتراح ، فإن ذعر وقت التشغيل ليس هو المشكلة ، ولكنه مجرد عرض - المشكلة هي الخطأ في المرور العرضي nil لشيء لا يتوقعه وأن هذا الخطأ يتم اكتشافه فقط في وقت التشغيل.

يحدث موقف مشابه مع طرق المستقبل غير المؤشر

أنا لا أشتري هذا القياس. المنظمة البحرية الدولية من المعقول تمامًا أن تؤخذ في الاعتبار

func Foo(p *int) { *p = 42 }

func main() {
    var v int
    Foo(&v)
    if v != 42 { panic("") }
}

أن تكون رمزًا صحيحًا. لا أعتقد أنه من المعقول أن تؤخذ في الاعتبار

func Foo(v int) { v = 42 }

func main( ){
    var v int
    Foo(v)
    if v != 42 { panic("") }
}

ليكون صحيحا. ربما إذا كنت مبتدئًا تمامًا في Go وكنت قادمًا من لغة تكون فيها كل قيمة مرجعية (على الرغم من أنني بصراحة مضغوطة بشدة للعثور على واحدة - حتى Python و Java يشيران إلى معظم القيم فقط). لكن IMO ، التحسين لهذه الحالة غير مجدي ، من العدل أن نفترض أن الناس لديهم بعض الإلمام بالمؤشرات مقابل القيم. أعتقد أنه حتى مطور Go المتمرس سوف ينظر ، على سبيل المثال ، إلى طريقة مع مستقبل مؤشر يصل إلى حقولها على أنها صحيحة ، وأن الكود الذي يستدعي هذه الأساليب صحيح. في الواقع ، هذه هي الحجة الكاملة لمنع nil -pointers بشكل ثابت ، أنه من السهل جدًا أن يكون المؤشر بدون قصد وأن رمز المظهر الصحيح يفشل في وقت التشغيل.

الإصلاح في كلتا الحالتين هو نفسه: تحقق من عدم وجود شيء قبل القيام بأي شيء.

إصلاح IMO في الدلالات الحالية هو عدم التحقق من

// The correct code would be:
if p == nil {
    // panic or return error
}
*p = 42

لكنني لا أعتبر هذا الرمز صحيحًا. لا يفعل الشيك nil أي شيء ، لأن إلغاء الإشارة إلى nil يثير الذعر بالفعل .

لكن ، بالنسبة لي ، أ) أكثر ضررًا (التطبيق بأكمله يتعطل!).

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

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

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

لقد كتبت _ "... يبدو أن السبب الرئيسي الذي يعيق ذلك هو ...." _ استنادًا إلى جملتك _ "هذا هو جوهر الموضوع" _ في إشارة إلى القيمة الصفرية. لهذا السبب افترضت أن القيمة الصفرية هي الشيء الرئيسي الذي يعيق ذلك. لذلك كان افتراضي السيئ.

فيما يتعلق بمعاملة nil بصمت مقابل فحصها في وقت الترجمة: أوافق على أنه من الأفضل التحقق منها في وقت الترجمة. كانت "القيمة الصفرية الديناميكية" مجرد تكرار للاقتراح الأصلي عندما ركزت على معالجة مشكلة جميع الأنواع يجب أن يكون لها قيمة صفرية. كان الدافع الإضافي هو أنني اعتقدت أنه كان أيضًا العائق الرئيسي لاقتراح النقابات التمييزية.
إذا ركزنا فقط على المشكلة المتعلقة بعدم وجود شيء ، فإنني أفضل أن يتم فحص أنواع المؤشرات غير الصفرية في وقت الترجمة.

أود أن أقول أنه في مرحلة ما ، سنحتاج (مع "نحن" أشير إلى مجتمع Go بأكمله) إلى قبول نوع ما من التغيير. على سبيل المثال: إذا كان هناك حل جيد لتجنب أخطاء nil تمامًا وكان الشيء الذي يمنع ذلك هو قرار التصميم "جميع الأنواع لها قيمة صفرية وهي مكونة من 0" ، فيمكننا التفكير في الفكرة لإجراء بعض التعديلات أو التغييرات على هذا القرار إذا كان يجلب قيمة.

السبب الرئيسي لقولي هذا هو جملتك _ "كل نوع له قيمة صفرية ، توقف كامل " _. أنا عادة لا أحب "كتابة التوقفات الكاملة". لا تفهموني خطأ! أنا أتقبل تمامًا أنك تفكر بهذه الطريقة ، إنها مجرد طريقة تفكيري: لا أفضل العقائد لأنها يمكن أن تخفي المسارات التي يمكن أن تؤدي إلى حلول أفضل.

أخيرًا ، بخصوص هذا:

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

وأنا أتفق تماما مع هذا. دائمًا ما يكون الفشل بصوت عالٍ أفضل من الفشل بصمت. ومع ذلك ، هناك مشكلة في Go:

  • إذا كان لديك تطبيق يحتوي على الآلاف من goroutines ، فإن الذعر غير المعالج في أحدهم يؤدي إلى تعطل البرنامج بالكامل. هذا يختلف عما هو عليه في اللغات الأخرى ، حيث يتعطل فقط الخيط الذي يسبب الذعر

وبغض النظر عن ذلك (على الرغم من أنه خطير للغاية) ، فإن الفكرة هي ، إذن ، لتجنب فئة كاملة من حالات الفشل (الفشل المرتبط nil ).

لذلك دعونا نستمر في تكرار هذا ونحاول إيجاد حل.

شكرا لك على وقتك وطاقتك!

أود أن أرى بناء جملة النقابات المميّزة في الصدأ بدلاً من أنواع مجموع haskell ، فهي تسمح بتسمية المتغيرات وتسمح باقتراح بناء جملة أفضل لمطابقة الأنماط.
يمكن أن يتم التنفيذ مثل البنية مع حقل العلامة (نوع uint ، يعتمد على عدد المتغيرات) وحقل الاتحاد (الاحتفاظ بالبيانات).
هذه الميزة مطلوبة لمجموعة متغيرات مغلقة (سيكون تمثيل الحالة أسهل بكثير وأنظف ، مع فحص وقت الترجمة). وفقًا للأسئلة حول الواجهات وتمثيلها ، أعتقد أن تنفيذها في نوع المجموع يجب ألا يتجاوز مجرد حالة أخرى من نوع المجموع ، لأن الواجهة تدور حول أي نوع يناسب بعض المتطلبات ، ولكن حالة استخدام نوع المجموع مختلفة.

بناء الجملة:

type Type enum {
         Tuple (int,int),
         One int,
         None,
};

في المثال أعلاه ، سيكون الحجم sizeof ((int ، int)).
يمكن إجراء مطابقة الأنماط باستخدام مشغل المطابقة الذي تم إنشاؤه حديثًا ، أو داخل مشغل التبديل الحالي ، تمامًا مثل:

var a Type
switch (a) {
         case Tuple{(b,c)}:
                    //do something
         case One{b}:
                    //do something else
         case None:
                    //...
}

بناء جملة الخلق:
var a Type = Type{One=12}
لاحظ أنه في إنشاء مثيل التعداد ، يمكن تحديد متغير واحد فقط.

القيمة الصفرية (مشكلة):
يمكننا فرز الأسماء بترتيب أبجدي ، وستكون القيمة الصفرية للتعداد صفرية لنوع العضو الأول في قائمة الأعضاء التي تم فرزها.

يتم تحديد حل PS لمشكلة القيمة الصفرية في الغالب بالاتفاق.

أعتقد أن الاحتفاظ بالقيمة الصفرية للمجموع كقيمة صفرية لحقل المجموع المحدد من قبل المستخدم قد يكون أقل إرباكًا ، ربما

أعتقد أن الاحتفاظ بالقيمة الصفرية للمجموع كقيمة صفرية لحقل المجموع المحدد من قبل المستخدم قد يكون أقل إرباكًا ، ربما

لكن جعل القيمة الصفرية يعتمد على أمر الإعلان عن الحقل ، أعتقد أنه أسوأ.

كتب شخص ما وثيقة التصميم؟

لدي واحد:
19412- التمييز_التحادي_و_باترن_المطابقة. md.zip

لقد غيرت هذا:

أعتقد أن الاحتفاظ بالقيمة الصفرية للمجموع كقيمة صفرية لحقل المجموع المحدد من قبل المستخدم قد يكون أقل إرباكًا ، ربما

الآن في اقتراحي ، انتقلت اتفاقية القيمة الصفرية (المشكلة) إلى موقف urandoms.

محدث: تم تغيير مستند التصميم وإصلاحات طفيفة.

لدي حالتا استخدام حديثتان ، حيث كنت بحاجة إلى أنواع مجموع مضمنة:

  1. تمثيل شجرة AST ، كما هو متوقع. في البداية ، عثروا على مكتبة كانت حلاً للوهلة الأولى ، لكن نهجهم كان وجود هيكل كبير به الكثير من الحقول التي لا يمكن nilable. أسوأ ما في العالمين IMO. لا يوجد نوع أمان بالطبع. كتب منطقتنا بدلا من ذلك.
  2. كان لدينا قائمة انتظار من مهام الخلفية المحددة مسبقًا: لدينا خدمة بحث قيد التطوير حاليًا وقد تكون عمليات البحث لدينا طويلة جدًا ، إلخ. لذلك قررنا تنفيذها في الخلفية عبر إرسال مهام تشغيل فهرس البحث إلى قناة. ثم سيقرر المرسل ما يجب فعله معهم. يمكن استخدام نمط الزائر ، ولكن من الواضح أنه مبالغة في طلب gRPC البسيط. وليس من الواضح بشكل خاص أن نقول على الأقل ، لأنه يقدم رابطًا بين المرسل والزائر.

في كلتا الحالتين تم تنفيذ شيء مثل هذا (في مثال المهمة الثانية):

type Task interface {
    task()
}

type SearchAdd struct {
    Ctx   context.Context
    ID    string
    Attrs Attributes
}

func (SearchAdd) task() {}

type SearchUpdate struct {
    Ctx         context.Context
    ID          string
    UpdateAttrs UpdateAttributes
}

func (SearchUpdate) task() {}

type SearchDelete struct {
    Ctx context.Context
    ID  string
}

func (SearchDelete) task() {}

وثم

task := <- taskChannel

switch v := task.(type) {
case tasks.SearchAdd:
    resp, err := search.Add(task.Ctx, &search2.RequestAdd{…}
    if err != nil {
        log.Error().Err(err).Msg("blah-blah-blah")
    } else {
        if resp.GetCode() != search2.StatusCodeSuccess  {
            …
        } 
    }
case tasks.SearchUpdate:
    …
case tasks.SearchDelete:
    …
}

هذا جيد تقريبًا. لا يوفر الشيء السيئ Go أمانًا كاملاً من النوع ، أي لن يكون هناك خطأ في التجميع بعد إضافة مهمة عملية فهرس البحث الجديدة.

يعد استخدام أنواع المجموع IMHO هو الحل الأكثر وضوحًا لهذا النوع من المهام التي يتم حلها عادةً مع الزائر ومجموعة من المرسلين ، حيث لا تكون وظائف الزائر كثيرة وصغيرة ويكون الزائر نفسه من النوع الثابت.

أنا أؤمن حقًا بوجود شيء مثل

type Task oneof {
    // SearchAdd holds a data for a new record in the search index
    SearchAdd {
        Ctx   context.Context
        ID    string
        Attrs Attributes   
    }

    // SearchUpdate update a record
    SearchUpdate struct {
        Ctx         context.Context
        ID          string
        UpdateAttrs UpdateAttributes
    }

    // SearchDelete delete a record
    SearchDelete struct {
        Ctx context.Context
        ID  string
    }
}

+

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

سيكون أكثر بروحًا من Goish أكثر من أي نهج آخر يسمح به Go في حالته الحالية. لا حاجة لمطابقة نمط Haskellish ، فالغوص حتى نوع معين أكثر من كافٍ.

أوتش ، غاب عن وجهة نظر اقتراح النحو. اصلحه.

إصداران ، أحدهما لنوع المجموع العام ونوع المجموع للتعدادات:

أنواع المجموع العام

type Sum oneof {
    T₁ TypeDecl₁
    T₂ TypeDecl₂
    …
    Tₙ TypeDeclₙ
}

حيث T₁Tₙ هي تعريفات النوع في نفس المستوى مع Sum ( oneof يعرضها خارج نطاقه) و Sum يعلن بعض الواجهة التي ترضي فقط T₁Tₙ .

المعالجة مشابهة لما لدينا مفتاح (type) ماعدا أنها تتم بشكل ضمني عبر كائنات oneof ويجب أن يكون هناك فحص مترجم إذا تم إدراج جميع المتغيرات.

تعداد آمن من النوع الحقيقي

type Enum oneof {
    Value = iota
}

تشبه إلى حد كبير iota من consts ، باستثناء القيم المدرجة صراحة فقط هي Enums وكل شيء آخر ليس كذلك.

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

سيكون أكثر بروحًا من Goish أكثر من أي نهج آخر يسمح به Go في حالته الحالية. لا حاجة لمطابقة نمط Haskellish ، فالغوص حتى نوع معين أكثر من كافٍ.

لا أعتقد أن التلاعب بمعنى المتغير task فكرة جيدة ، على الرغم من أنها مقبولة.

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

سيكون أكثر بروحًا من Goish أكثر من أي نهج آخر يسمح به Go في حالته الحالية. لا حاجة لمطابقة نمط Haskellish ، فالغوص حتى نوع معين أكثر من كافٍ.

لا أعتقد أن التلاعب بمعنى متغير المهمة فكرة جيدة ، على الرغم من أنها مقبولة.
""

حظا سعيدا مع زوار موقعك بعد ذلك.

sirkon ماذا تقصد بالزوار؟ أعجبني بناء الجملة هذا ، ولكن يجب كتابة المفتاح على النحو التالي:

switch task {
case Task.SearchAdd:
    // task is Task.SearchAdd in this scope
case Task.SearchUpdate:
case Task.SearchDelete:
}

وأيضًا ما هي قيمة لا قيمة لـ Task ؟ على سبيل المثال:

var task Task

هل سيكون nil ؟ إذا كان الأمر كذلك ، فهل يجب أن يكون لـ switch case nil إضافي؟
أم سيتم تهيئته إلى النوع الأول؟ سيكون هذا أمرًا محرجًا ، لأن ترتيب إعلان النوع مهم بطريقة لم تكن كذلك من قبل ، ولكن من المحتمل أن يكون مناسبًا للتعدادات الرقمية.

أفترض أن هذا يعادل switch task.(type) لكن التبديل سيتطلب وجود جميع الحالات ، أليس كذلك؟ كما في .. إذا فاتتك حالة واحدة ، خطأ في التجميع. ولا يُسمح default . هل هذا صحيح؟

ماذا تقصد بالزوار؟

قصدت أنها الخيار الآمن الوحيد في Go لمثل هذا النوع من الوظائف. أسوأ بكثير في ذلك بالنسبة لمجموعة معينة من الحالات (عدد محدود من البدائل المحددة مسبقًا).

وأيضًا ما هي القيمة التي لا قيمة لها للمهمة؟ على سبيل المثال:

var task Task

أخشى أنه يجب أن يكون من النوع nilable في Go مثل هذا

أم سيتم تهيئته إلى النوع الأول؟

سيكون غريبًا جدًا خاصةً لغرض مقصود.

أفترض أن هذا يعادل تبديل المهمة. (النوع) لكن التبديل يتطلب أن تكون جميع الحالات موجودة ، أليس كذلك؟ كما في .. إذا فاتتك حالة واحدة ، خطأ في التجميع.

نعم صحيح.

ولا يسمح بالتقصير. هل هذا صحيح؟

لا ، الافتراضيات مسموح بها. رغم الإحباط.

ملاحظة: يبدو أنني حصلت على فكرة لدى Goianlancetaylor وأشخاص آخرين من Go حول أنواع المجموع. يبدو أن الصفري يجعلهم عرضة لـ NPD ، لأن Go ليس لديه أي سيطرة على قيم الصفر.

إذا كان لا شيء ، فأعتقد أنه بخير. أفضل أن يكون case nil متطلبًا لكشف التبديل. إن القيام بـ if task != nil قبل أمر جيد أيضًا ، أنا فقط لا أحب ذلك كثيرًا: |

هل هذا مسموح به أيضا؟

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

هل هذا مسموح به أيضا؟

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

حسنًا ، لا توجد ثوابت بعد ذلك ، فقط

type Foo oneof {
    A <type reference>
}

أو

type Foo oneof {
    A = iota
    B
    C
}

أو

type Foo oneof {
    A = 1
    B = 2
    C = 3
}

لا يوجد مزيج من iotas والقيم. أو بالاقتران مع التحكم في القيم ، لا ينبغي تكرارها.

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

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

على أي حال. فقط اعتقدت أنها مثيرة للاهتمام.

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

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

قد يصبح:

contract intOrFloat64(T) {
    T int, float64
}

func addOne(type T intOrFloat64) (x T) T {
    return x + 1
}

فيما يتعلق بأنواع المجموع نفسها ، إذا هبطت الأدوية الجنيسة في النهاية ، سأكون أكثر شكًا مما أنا عليه الآن بشأن ما إذا كانت فوائد تقديمها ستفوق تكاليف لغة بسيطة مثل Go.

ومع ذلك ، إذا تم القيام بشيء ما ، فإن الحل الأبسط والأقل تخريبًا IMO سيكون فكرة

type intOrFloat64 interface{ type int, float64 }    

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

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

var v1 intOrFloat64 = 1        // compiles, dynamic type int
var v2 intOrFloat64 = 1.0      // compiles, dynamic type float64
var v3 intOrFloat64 = 1 + 2i   // doesn't compile, complex128 is not a specified type

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

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

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

IIUC لن يكون الشيء الجنسي من الأنواع الديناميكية ، لذا فإن هذه النقطة برمتها لا تصمد. ومع ذلك ، إذا تم السماح للواجهات بالعمل كعقود (وهو ما أشك فيه) ، فلن تحل عمليات الفحص والتعداد الشاملة التي (أعتقد ، ربما لا؟) هي عبارة عن سومتيبس.

alanfo ، Mirovius شكرا على جديلة ؛ من المثير للاهتمام أن هذه المناقشة تتحول في هذا الاتجاه:

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

@نبيذ جيد
لم أكن أقترح أن تصميم الأدوية الجنسية سيتناول كل شيء قد يرغب المرء في القيام به مع أنواع المجموع - كما أوضح

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

بقدر ما يتعلق الأمر بالواجهات المقيدة ، سيعرف المترجم الأنواع الدقيقة التي يمكن استخدامها ، وبالتالي يصبح من الممكن القيام بفحص شامل في عبارة تبديل النوع.

تضمين التغريدة

إنني في حيرة من أمر ما قلته لأنني أعتقد أن مسودة وثيقة تصميم الأدوية الجنيسة تشرح بوضوح تام (في قسم "لماذا لا تستخدم الواجهات بدلاً من العقود") لماذا اعتُبرت الأخيرة وسيلة أفضل من السابقة للتعبير عن القيود العامة.

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

لا يمكن قول الشيء نفسه عن واجهة ، معلمات أم لا. إذا كان لديهم أي قيود على الإطلاق ، فسيحتاج كل نوع معلمة إلى واجهة منفصلة.

هذا يجعل الأمر أكثر صعوبة للتعبير عن علاقة بين معلمات النوع باستخدام الواجهات وإن لم يكن ذلك مستحيلًا كما هو موضح في مثال الرسم البياني.

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

أعتقد أنه لن يكون من المهم حقًا ما إذا كانت قيود نوع الواجهة يمكن أن تتضمن أنواعًا "غير مضمنة" بقدر ما يتعلق الأمر بالأدوية ، على الرغم من الحاجة إلى إيجاد طريقة ما لتقييدها بالأنواع الدقيقة (وليس الأنواع المشتقة أيضًا) لذلك ستكون مناسبة لأنواع المجموع. ربما يمكننا استخدام const type للأخير (أو حتى const ) إذا أردنا التمسك بالكلمات الرئيسية الحالية.

griesemer هناك عدة أسباب تجعل أنواع الواجهات ذات المعلمات ليست بديلاً مباشرًا للعقود.

  1. معلمات النوع هي نفسها الموجودة في الأنواع الأخرى ذات المعلمات.
    في نوع مثل

    type C2(type T C1) interface { ... }
    

    معلمة النوع T موجودة خارج الواجهة نفسها. أي نوع من الوسيطة التي تم تمريرها كـ T يجب أن يكون معروفًا بالفعل أنه يفي بالعقد C1 ، ولا يمكن لنص الواجهة أن يقيد T . هذا يختلف عن معايير العقد ، والتي يتم تقييدها من خلال نص العقد نتيجة تمريره إليه. قد يعني هذا أن كل معلمة نوع لوظيفة ما يجب أن تكون مقيدة بشكل مستقل قبل أن يتم تمريرها كمعامل إلى القيد على أي معلمة نوع أخرى.

  2. لا توجد طريقة لتسمية نوع جهاز الاستقبال في نص الواجهة.
    يجب أن تسمح لك الواجهات بكتابة شيء مثل:

    type C3(type U C1) interface(T) {
        Add(T) T
    }
    

    حيث يشير T إلى نوع المستلم.

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

بالنسبة للتمييز بين قيود النوع الأساسية وقيود هوية النوع ، هناك طريقة واحدة قد تنجح. تخيل أنه يمكننا تحديد قيود مخصصة ، مثل

contract (T) indenticalTo(U) {
    *T *U
}

(هنا ، أنا أستخدم تدوينًا مخترعًا لتحديد نوع واحد على أنه "المتلقي". سأعلن عقدًا مع نوع مستقبل صريح باسم "قيد" ، تمامًا كما يُنطق func مع جهاز استقبال "طريقة". المعلمات بعد اسم العقد هي معلمات نوع عادي ولا يمكن أن تظهر على الجانب الأيسر من عبارة القيد في نص القيد.)

نظرًا لأن النوع الأساسي لنوع المؤشر الحرفي هو نفسه ، فإن هذا القيد يعني أن T مطابق لـ U . نظرًا لأنه تم التصريح عن هذا باعتباره قيدًا ، يمكنك كتابة (identicalTo(int)), (identicalTo(uint)), ... كفصل قيد.

بينما قد تكون العقود مفيدة للتعبير عن نوع من أنواع المجموع ، لا أعتقد أنه يمكنك التعبير عن أنواع المجموع العامة معهم. من خلال ما رأيته من المسودة ، يتعين على المرء أن يسرد الأنواع الملموسة ، لذلك لا يمكنك كتابة شيء مثل هذا:

contract Foo(T, U) {
    T U, int64
}

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

لقد كنت أفكر أكثر في كيفية تغيير تصميم المسودة إذا تم تمديد الواجهات لتشمل قيود النوع ثم استخدامها لاستبدال العقود في التصميم.

ربما يكون من الأسهل تحليل الموقف إذا أخذنا في الاعتبار عددًا مختلفًا من معلمات النوع:

لا توجد معلمات

لا تغيير :)

معلمة واحدة

لا توجد مشاكل حقيقية هنا. ستكون هناك حاجة إلى واجهة ذات معلمات (على عكس الواجهة غير العامة) فقط إذا كانت معلمة النوع تشير إلى نفسها و / أو بعض الأنواع الثابتة المستقلة الأخرى مطلوبة لإنشاء مثيل للواجهة.

معلمتان أو أكثر

كما ذكرنا سابقًا ، يجب تقييد كل معلمة من النوع بشكل فردي إذا كانت بحاجة إلى قيد على الإطلاق.

لن تكون هناك حاجة إلى واجهة ذات معلمات إلا إذا:

  1. تشير معلمة النوع إلى نفسها.

  2. أشارت الواجهة إلى معلمة من نوع آخر أو معلمات _ تم الإعلان عنها بالفعل_ في قسم معلمة النوع (من المفترض أننا لا نريد التراجع هنا).

  3. كانت هناك حاجة إلى بعض الأنواع الثابتة المستقلة الأخرى لإنشاء مثيل للواجهة.

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

ومع ذلك ، كما هو موضح في مستند التصميم ، يمكنك التغلب على هذا من خلال الإعلان عن عدم وجود معلمات (حيث لا يشيرون إلى أنفسهم) NodeInterface و EdgeInterface في المستوى الأعلى حيث لن تكون هناك مشكلة في الإشارة إلى بعضهم البعض مهما كانت أمر إعلان. يمكنك بعد ذلك استخدام هذه الواجهات لتقييد معلمات نوع بنية الرسم البياني وتلك الخاصة بالطريقة "الجديدة" المرتبطة بها.

لذلك لا يبدو أن هناك أي مشاكل مستعصية هنا حتى لو كانت فكرة العقود أجمل.

من المفترض أن يصبح comparable الآن مجرد واجهة مضمنة بدلاً من عقد.

يمكن بالطبع تضمين الواجهات في بعضها البعض كما يمكن بالفعل.

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

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

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

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

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

interface Foo(T) {
    const type T, int64  // 'const' indicates types are exact i.e. no derived types
}

لماذا لا يمكننا فقط إضافة النقابات التمييزية إلى اللغة بدلاً من اختراع جولة أخرى حول غيابهم؟

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

urandom

لا أعتقد أنه يمكنك التعبير عن أنواع المجموع العام معهم

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

تضمين التغريدة

هذا يجعل الأمر أكثر صعوبة للتعبير عن علاقة بين معلمات النوع باستخدام الواجهات وإن لم يكن ذلك مستحيلًا كما هو موضح في مثال الرسم البياني.

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

لكن TBH ، لا أعتقد أن أيًا منهما محرج حقًا إلى حد ما - فأنت تكتب إلى حد كبير نفس الرمز بالضبط للتعبير إلى حد كبير عن نفس الأشياء تمامًا. و FWIW ، هناك حالة فنية سابقة لهذا. تتصرف فئات نوع Haskell كثيرًا مثل الواجهات وكما تشير مقالة wiki هذه ، فإن استخدام فئات النوع متعددة المعلمات للتعبير عن العلاقات بين الأنواع أمر طبيعي جدًا.

تضمين التغريدة

لا توجد طريقة لتسمية نوع جهاز الاستقبال في نص الواجهة.

الطريقة التي ستتعامل بها مع وسيطات النوع في موقع الاستخدام. بمعنى آخر

type Adder(type T) interface {
    Add(t T) T
}

func Sum(type T Adder(T)) (vs []T) T {
    var t T
    for _, v := range vs {
        t = t.Add(v)
    }
    return t
}

يتطلب هذا بعض العناية فيما يتعلق بكيفية عمل التوحيد ، بحيث يمكنك السماح بمعلمات النوع المرجعية الذاتية ، لكنني أعتقد أنه يمكن جعلها تعمل.

1. و 3. أنا لا أفهم حقًا ، يجب أن أعترف. سأستفيد من بعض الأمثلة الملموسة.


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

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

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

حظا سعيدا مع زوار موقعك بعد ذلك.

لماذا تعتقد أن مطابقة الأنماط لا يمكن إجراؤها في Go؟ إذا كنت تفتقر إلى أمثلة مطابقة باتن ، فراجع ، على سبيل المثال ، Rust.

@ Merovius re: "بالنسبة لي ، الرسم البياني هو كيان"

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

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

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

لاقتراح تقليل الأخطاء التي تحدث حاليًا في كل مكان باستخدام مخططات interface{} ، ولكن لإزالة الكتابة المستمرة لعامل التشغيل | ، أقترح ما يلي:

type foobar union {
    int
    float64
}

مجرد حالة الاستخدام وحدها لاستبدال العديد من interface{} بهذا النوع من الأمان سيكون مكسبًا هائلاً للمكتبة. مجرد النظر إلى نصف الأشياء في مكتبة التشفير يمكن أن يستخدم هذا.

مشكلات مثل: آه لقد أعطيت ecdsa.PrivateKey بدلاً من *ecdsa.PrivateKey - إليك خطأ عام يدعم فقط ecdsa.PrivateKey. الحقيقة البسيطة المتمثلة في أن هذه الأنواع يجب أن تكون واضحة من النقابات ستزيد من أمان النوع بدرجة كبيرة.

بينما يشغل هذا الاقتراح مساحة _ أكبر مقارنة بـ int|float64 فإنه يجبر المستخدم على التفكير في هذا الأمر. الحفاظ على نظافة قاعدة الكود كثيرًا.

لاقتراح تقليل الأخطاء التي تحدث حاليًا في كل مكان باستخدام مخططات interface{} ، ولكن لإزالة الكتابة المستمرة لعامل التشغيل | ، أقترح ما يلي:

type foobar union {
    int
    float64
}

مجرد حالة الاستخدام وحدها لاستبدال العديد من interface{} بهذا النوع من الأمان سيكون مكسبًا هائلاً للمكتبة. مجرد النظر إلى نصف الأشياء في مكتبة التشفير يمكن أن يستخدم هذا.

مشكلات مثل: آه لقد أعطيت ecdsa.PrivateKey بدلاً من *ecdsa.PrivateKey - إليك خطأ عام يدعم فقط ecdsa.PrivateKey. الحقيقة البسيطة المتمثلة في أن هذه الأنواع يجب أن تكون واضحة من النقابات ستزيد من أمان النوع بدرجة كبيرة.

بينما يشغل هذا الاقتراح مساحة _ أكبر مقارنة بـ int|float64 فإنه يجبر المستخدم على التفكير في هذا الأمر. الحفاظ على نظافة قاعدة الكود كثيرًا.

انظر هذا (تعليق) ، إنه اقتراحي.

في الواقع يمكننا تقديم أفكارنا إلى اللغة. سيؤدي ذلك إلى وجود طريقتين أصليتين للقيام بـ ADT ، ولكن مع صيغ مختلفة.

اقتراحي للميزات ، وخاصة مطابقة الأنماط ، من أجل التوافق والقدرة على الاستفادة من ميزة قواعد التعليمات البرمجية القديمة.

لكن يبدو وكأنه مبالغة ، أليس كذلك؟

أيضًا ، يمكن إنشاء نوع المجموع ليكون قيمته الافتراضية nil . بالطبع سيتطلب الأمر nil حالة في كل تبديل.
يمكن إجراء مطابقة الأنماط مثل:
-- إعلان

type U enum{
    A(int64),
    B(string),
}

- مطابقة

...
var a U
...
switch a {
    case A{b}:
         //process b here
    case B{b}:
         //...
    case nil:
         //...
}
...

إذا كان أحد لا يحب مطابقة النمط - انظر اقتراح sirkon أعلاه.

أيضًا ، يمكن إنشاء نوع المجموع ليكون قيمته الافتراضية nil . بالطبع سيتطلب الأمر nil حالة في كل تبديل.

ألن يكون من الأسهل عدم السماح بقيمة لم يتم بدؤها في وقت التجميع؟ بالنسبة للحالات التي نحتاج فيها إلى قيمة تمت تهيئتها ، يمكننا إضافتها إلى نوع المجموع: ie

type U enum {
  None
  A(string)
  B(uint64)
}
...
var a U.None
...
switch a {
  case U.None: ...
  case U.A(str): ...
  case U.B(i): ...
}

أيضًا ، يمكن إنشاء نوع المجموع ليكون قيمته الافتراضية nil . بالطبع سيتطلب الأمر nil حالة في كل تبديل.

ألن يكون من الأسهل عدم السماح بقيمة لم يتم بدؤها في وقت التجميع؟ بالنسبة للحالات التي نحتاج فيها إلى قيمة تمت تهيئتها ، يمكننا إضافتها إلى نوع المجموع: ie

يكسر الكود الموجود.

أيضًا ، يمكن إنشاء نوع المجموع ليكون قيمته الافتراضية nil . بالطبع سيتطلب الأمر nil حالة في كل تبديل.

ألن يكون من الأسهل عدم السماح بقيمة لم يتم بدؤها في وقت التجميع؟ بالنسبة للحالات التي نحتاج فيها إلى قيمة تمت تهيئتها ، يمكننا إضافتها إلى نوع المجموع: ie

يكسر الكود الموجود.

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

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

لقد اتفقت معك على الفكرة الأولى ، ولكن بعد بعض التفكير ، كان من الممكن استخدام الاسم الجديد المحجوز للاتحاد مسبقًا في بعض قواعد الكود (union ، enum ، إلخ.)

أعتقد أن الالتزام بالتحقق من عدم وجود شيء سيكون مؤلمًا جدًا للاستخدام.

يبدو أنه تغيير جذري للتوافق مع الإصدارات السابقة لا يمكن حله إلا من خلال Go2.0

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

ولكن هناك الكثير من كود go الحالي الذي يحتوي على كل شيء لا شيء. هذا بالتأكيد سوف يكسر التغيير. والأسوأ من ذلك ، يمكن لـ gofix والأدوات المماثلة فقط تغيير أنواع المتغيرات إلى خيارات (من نفس النوع) لإنتاج رمز قبيح على الأقل ، وكل الحالات الأخرى ستكسر ببساطة كل شيء في العالم.

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

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

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

الآن ، دعنا نفكر في html / template.contentType. النوع العادي هو قيمته الافتراضية ، ويتم التعامل معه بالفعل على هذا النحو من خلال وظيفة Stringify ، لأنه احتياطي. في مستقبل المجموع الافتراضي ، لن تظل بحاجة إلى هذا السلوك فحسب ، بل سيكون من غير المجدي أيضًا استخدام قيمة صفرية له ، لأن لا شيء لن يعني أي شيء لمستخدم من هذا النوع. سيكون من الإلزامي إلى حد كبير إرجاع قيمة مسماة هنا دائمًا ، ولديك افتراضي واضح لما يجب أن تكون عليه هذه القيمة.

أنا مرة أخرى مع مثال آخر حيث تعمل أنواع البيانات الجبرية / المتغيرة / المجموع / أيًا كانت أنواع البيانات بشكل جيد.

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

لدي مهمة جديدة لكتابة مجموعة من الكيانات التي يمكن إدراجها أو إلحاقها أو حذفها (واحدة فقط من هذه العمليات).

إذا كان بإمكاننا الحصول على شيء مثل

type EntityOp oneof {
    Insert   Reference
    NewState string
    Delete   struct{}
}

يمكن أن تكون الطريقة عادلة

type DB interface {
    …
    Capture(ctx context.Context, processID string, ops map[string]EntityOp) (bool, error)
}

أحد الاستخدامات الرائعة لمجموع الأوقات هو تمثيل العقد في AST. واحد آخر هو استبدال nil بـ option الذي يتم فحصه في وقت الترجمة.

DemiMarie ولكن في Go اليوم ، يمكن أن يكون هذا المبلغ أيضًا

لا أعرف ما إذا كان ينتمي هنا ، ولكن كل هذا يبقى لي طباعة نصية ، حيث توجد ميزة رائعة جدًا تسمى "String Literal Types" ويمكننا القيام بذلك:

var name: "Peter" | "Consuela"; // string type with compile-time constraint

إنها مثل سلسلة التعداد ، وهي أفضل بكثير من التعداد الرقمي التقليدي في رأيي.

تضمين التغريدة
مثال ملموس هو العمل مع JSON التعسفي.
في Rust يمكن تمثيله كـ
قيمة التعداد {
باطل،
منطقي (منطقي) ،
رقم (رقم) ،
سلسلة (سلسلة) ،
صفيف (Vec) ،
كائن (خريطة) ،
}

نوع الاتحاد كميزتين:

  1. التوثيق الذاتي للكود
  2. السماح للمترجم أو go vet بالتحقق من الاستخدام غير الصحيح لنوع الاتحاد
    (على سبيل المثال ، مفتاح حيث لا يتم فحص جميع الأنواع)

بالنسبة إلى بناء الجملة ، يجب أن يكون ما يلي متوافقًا مع Go1 ، كما هو الحال مع الاسم المستعار من النوع :

type Token = int | float64 | string

يمكن تنفيذ نوع الاتحاد داخليًا كواجهة ؛ المهم هو أن استخدام نوع الاتحاد يسمح للكود أن يكون أكثر قابلية للقراءة ويصطاد الأخطاء مثل

var tok Token

switch t := tok.(type) {
case int:
    // do something
}

يجب أن يقوم المترجم بإصدار خطأ ، حيث لا يتم استخدام جميع أنواع Token في المحول.

المشكلة في هذا ، أنه لا توجد (على حد علمي) طريقة لتخزين أنواع المؤشرات (أو الأنواع التي تحتوي على مؤشرات ، مثل string ) وأنواع غير المؤشرات معًا. حتى الأنواع ذات التخطيطات المختلفة لن تعمل. لا تتردد في التصحيح لي ولكن المشكلة هي أن GC الدقيق لا يعمل بشكل جيد مع المتغيرات التي يمكن أن تكون مؤشرات ومتغيرات بسيطة في نفس الوقت.

يمكننا السير في طريق الملاكمة الضمنية - مثلما يفعل حاليًا interface{} . لكنني لا أعتقد أن هذا يوفر فوائد كافية - لا يزال يبدو وكأنه نوع واجهة ممجدة. ربما يمكن تطوير نوع من الشيك vet بدلاً من ذلك؟

سيحتاج جامع القمامة إلى قراءة بتات العلامة من الاتحاد لتحديد التخطيط. هذا ليس مستحيلًا ولكنه سيكون تغييرًا كبيرًا في وقت التشغيل قد يؤدي إلى إبطاء gc.

ربما يمكن تطوير نوع من الفحص البيطري بدلاً من ذلك؟

https://github.com/BurntSushi/go-sumtype

سيحتاج جامع القمامة إلى قراءة بتات العلامة من الاتحاد لتحديد التخطيط.

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

go-sumtype مثير للاهتمام ، شكرًا. ولكن ماذا يحدث إذا حددت الحزمة نفسها نوعين من النقابات؟

يمكن للمترجم تنفيذ نوع الاتحاد داخليًا كواجهة ، ولكن مع إضافة بناء جملة موحد وفحص قياسي للنوع.

إذا كانت هناك مشاريع N تستخدم أنواع الاتحاد ، كل منها بشكل مختلف ومع N كبيرة بما يكفي ، فربما يكون تقديم طريقة واحدة للقيام بذلك هو الحل الأفضل.

ولكن ماذا يحدث إذا حددت الحزمة نفسها نوعين من النقابات؟

لا شىء اكثر؟ المنطق حسب النوع ويستخدم طريقة وهمية للتعرف على المنفذين. فقط استخدم أسماء مختلفة للطرق الوهمية.

skybrian IIRC الصورة النقطية الحالية التي تحدد تخطيط النوع مخزنة حاليًا في مكان واحد. ستضيف إضافة مثل هذا الشيء لكل كائن الكثير من القفزات ، وستجعل كل كائن اختياري جذر GC.

المشكلة في هذا ، أنه لا توجد (على حد علمي) طريقة لتخزين أنواع المؤشرات (أو الأنواع التي تحتوي على مؤشرات ، مثل السلسلة) وأنواع غير المؤشرات معًا

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

لكنني لا أعتقد أن هذا يوفر فوائد كافية - لا يزال يبدو وكأنه نوع واجهة ممجدة.

في اقتراحي ، تعتبر أنواع الاتحاد _ بالضبط_ نوع واجهة تمجيد - نوع الاتحاد هو مجرد مجموعة فرعية من الواجهة التي يُسمح لها فقط بتخزين مجموعة من الأنواع التي تم تعدادها. من المحتمل أن يمنح هذا المترجم حرية اختيار طريقة تخزين أكثر كفاءة لمجموعات أنواع معينة ، ولكن هذا هو تفاصيل التنفيذ ، وليس الدافع الرئيسي.

rogpeppe - بدافع الفضول ، هل يمكنني استخدام نوع المجموع مباشرةً أم أحتاج صراحةً إلى

يمكنني أن أفعل

type FooType int | float64

func AddOne(foo FooType) FooType {
    return foo + 1
}

// if this can be done, what happens here?
type FooType nil | int
func AddOne(foo FooType) FooType {
    return foo + 1
}

إذا كان هذا لا يمكن القيام به ، فلا أرى الكثير من الاختلاف معها

type FooType interface{}

func AddOne(foo FooType) (FooType, error) {
    switch v := foo.(type) {
        case int:
              return v + 1, nil
        case float64:
              return v + 1.0, nil
    }

    return nil, fmt.Errorf("invalid type %T", foo)
}

// versus
type FooType int | float64

func AddOne(foo FooType) FooType {
    switch v := foo.(type) {
        case int:
              return v + 1
        case float64:
              return v + 1.0
    }

    // assumes the compiler knows that there is no other type is 
    // valid and thus this should always returns a value
    // Would the compiler error out on incomplete switch types?
}

xibz

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

rogpeppe ، يرجى تصحيح لي إذا كنت مخطئا 🙏
إن الاضطرار دائمًا إلى إجراء مطابقة النمط (هكذا تسمى "casting" عند العمل مع أنواع الجمع في لغات البرمجة الوظيفية) هو في الواقع أحد أكبر فوائد استخدام أنواع الجمع. إن إجبار المطور على التعامل بشكل صريح مع جميع الأشكال الممكنة لنوع المجموع هو وسيلة لمنع المطور من استخدام متغير يعتقد أنه من نوع معين بينما هو في الواقع نوع مختلف. مثال مبالغ فيه ، في JavaScript:

const a = "1" // string "1"
const b = a + 5 // string "15" and not number 6

إذا كان هذا لا يمكن القيام به ، فلا أرى الكثير من الاختلاف معها

أعتقد أنك تذكر بعض المزايا بنفسك ، أليس كذلك؟

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

// Would the compiler error out on incomplete switch types?

بناءً على ما تفعله لغات البرمجة الوظيفية ، أعتقد أن هذا يجب أن يكون ممكنًا وقابلًا للتكوين 👍

xibz أيضًا أداء نظرًا لأنه يمكن القيام بذلك في وقت التجميع مقابل وقت التشغيل ، ولكن بعد ذلك هناك أدوية جنيسة ، آمل ، قبل يوم واحد من وفاتي.

xibz

بدافع الفضول ، هل يمكنني استخدام نوع المجموع مباشرةً أم أحتاج صراحةً إلى تحويله إلى نوع معروف لفعل أي شيء به؟

يمكنك استدعاء طرق عليه إذا كان جميع أعضاء النوع يشتركون في هذه الطريقة.

بأخذ int | float64 كمثال ، ماذا ستكون نتيجة:

var x int|float64 = int(2)
var y int|float64 = float64(0.5)
fmt.Println(x * y)

هل ستؤدي إلى تحويل ضمني من int إلى float64 ؟ أو من float64 إلى int . أم أنها ستصاب بالذعر؟

إذن أنت على حق تقريبًا - ستحتاج إلى التحقق من الكتابة قبل استخدامه في معظم الحالات. أعتقد أن هذه ميزة وليست عيبًا.

قد تكون ميزة وقت التشغيل كبيرة ، راجع للشغل. للاستمرار في كتابة المثال الخاص بك ، لن تحتاج شريحة من النوع [](int|float64) إلى احتواء أي مؤشرات لأنه من الممكن تمثيل جميع مثيلات النوع ببضع بايت (ربما 16 بايت بسبب قيود المحاذاة) ، والتي يمكن يؤدي إلى تحسينات كبيرة في الأداء في بعض الحالات.

تضمين التغريدة

ستحتاج إلى التحقق من الكتابة قبل استخدامه في معظم الحالات. أعتقد أن هذه ميزة وليست عيبًا.

أوافق على عدم كونه عيبًا. أحاول فقط معرفة الفوائد التي يمنحنا إياها هذا مقارنة بالواجهات.

لن تحتاج شريحة من النوع

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

@ stouf

إن الاضطرار دائمًا إلى إجراء مطابقة النمط (هكذا تسمى "casting" عند العمل مع أنواع الجمع في لغات البرمجة الوظيفية) هو في الواقع أحد أكبر فوائد استخدام أنواع الجمع

ولكن ما هي الفوائد التي تجلبها للغة التي لم يتم التعامل معها بالفعل مع الواجهات؟ في الأصل كنت مع أنواع المجموع تمامًا ، لكن عندما بدأت أفكر في الأمر ، فقدت نوعًا ما الفوائد التي ستجلبها.


مع كل ما قيل ، إذا كان استخدام نوع sum يمكن أن يوفر كودًا أنظف وأكثر قابلية للقراءة ، فسأكون 100٪ بالنسبة له. ومع ذلك ، من مظهره ، يبدو أنه سيبدو متطابقًا تقريبًا مع رمز الواجهة.

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

هذا مفتعل قليلاً ، ولكن على سبيل المثال إذا كان لديك شجرة تركيب تعبير ، لمطابقة معادلة من الدرجة الثانية ، يمكنك القيام بشيء مثل:

match Add(Add(Mult(Const(a), Power(Var(x), 2)), Mult(Const(b), Var(x))), Const(c)) {
  // here a, b, c are bound to the constants and x is bound to the variable name.
  // x must have been the same in both var expressions or it wouldn't match.
}

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

لست متأكدًا من مقدار ما يتم طرحه خارج المترجمين ، على الرغم من ذلك.

xibz
فائدة أنواع المجموع هي أنك أنت والمجمع يعرفان بالضبط الأنواع التي يمكن أن توجد داخل المجموع. هذا هو الاختلاف الأساسي. مع الواجهات الفارغة ، سيكون عليك دائمًا القلق والحماية من سوء الاستخدام في واجهة برمجة التطبيقات ، من خلال وجود فرع هدفه الوحيد هو التعافي عندما يمنحك المستخدم نوعًا لا تتوقعه.

نظرًا لأنه يبدو أن هناك أملًا ضئيلًا في تنفيذ أنواع المجموع في المترجم ، آمل أن يتم اقتراح توجيه تعليق قياسي على الأقل ، مثل //go:union A | B | C ودعمه go vet .

باستخدام طريقة قياسية للإعلان عن نوع المجموع ، سيكون من الممكن بعد N سنوات معرفة عدد الحزم التي تستخدمها.

مع مسودات التصميم الأخيرة للأدوية ، ربما يمكن ربط أنواع المجموع بها.

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

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

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

وربما يتطلب استخدام هذه الواجهات المحددة داخل محولات النوع أن يكون هذا المفتاح شاملاً.

هذه ليست الصيغة القصيرة المثالية (على سبيل المثال: Foo | int32 | []Bar ) ، لكنها شيء ما.

مع مسودات التصميم الأخيرة للأدوية ، ربما يمكن ربط أنواع المجموع بها.

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

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

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

وربما يتطلب استخدام هذه الواجهات المحددة داخل محولات النوع أن يكون هذا المفتاح شاملاً.

هذه ليست الصيغة القصيرة المثالية (على سبيل المثال: Foo | int32 | []Bar ) ، لكنها شيء ما.

تشبه إلى حد كبير اقتراحي: https://github.com/golang/go/issues/19412#issuecomment -520306000

type foobar union {
  int
  float
}

mathieudevos نجاح باهر ، أنا أحب ذلك في الواقع.

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

يعمل مفهوم union بشكل رائع في ذهني لأنه يمكنك بعد ذلك تضمين union في interface لإنجاز "قيد يتضمن أساليب وأنواع أولية". تستمر الواجهات في العمل كما هي ، ومع تعريف الدلالات حول الاتحاد ، يمكن استخدامها في الكود العادي ويختفي الشعور بالغرابة.

// Ordinary interface
type Stringer interface {
    String() string
}

// Type union
type Foobar union {
    int
    float
}

// Equivalent to an interface with a type list
type FoobarStringer interface {
    Stringer
    Foobar
}

// Unions can intersect
type SuperFoo union {
    Foobar
    int
}

// Doesn't compile since these unions can't intersect
type Strange interface {
    Foobar
    union {
        int
        string
    }
}

تحرير - في الواقع ، رأيت للتو هذا CL: https://github.com/golang/go/commit/af48c2e84b52f99d30e4787b1b8d527b5cd2ab64

الفائدة الأساسية من هذا التغيير أنه يفتح الباب أمام عامة الناس
(غير مقيد) استخدام واجهات مع قوائم النوع

...رائعة! تصبح الواجهات قابلة للاستخدام بالكامل كأنواع مجموع ، والتي توحد الدلالات عبر الاستخدام المنتظم والقيد. (من الواضح أنه لم يتم تشغيله بعد ، لكنني أعتقد أن هذه وجهة رائعة يجب التوجه إليها).

لقد فتحت # 41716 لمناقشة الطريقة التي تظهر بها نسخة من أنواع المجموع في مسودة تصميم الأدوية الحالية.

أردت فقط مشاركة اقتراح قديم من henryas حول أنواع البيانات الجبرية. إنه جيد جدًا مكتوبًا مع حالات الاستخدام المقدمة.
https://github.com/golang/go/issues/21154
لسوء الحظ ، تم إغلاقه بواسطة mvdan في نفس اليوم دون أي تقدير للعمل. أنا متأكد من أن هذا الشخص شعر بهذه الطريقة حقًا ، وبالتالي لا توجد أنشطة أخرى على حساب gh. أشعر بالأسف لذلك الرجل.

أنا حقا أحب # 21154. يبدو أنه شيء مختلف على الرغم من (ومن ثم تعليقmvdan ) إغلاقها

نعم ، أود حقًا أن أمتلك القدرة على نمذجة بعض منطق الأعمال عالي المستوى بطريقة مماثلة كما هو موضح في هذه المشكلة. ستكون أنواع المجموع للخيارات المقيدة التي تشبه التعداد والأنواع المقبولة المقترحة كما في المشكلة الأخرى رائعة في مربع الأدوات. في بعض الأحيان ، يبدو رمز العمل / المجال في Go مشوشًا بعض الشيء في الوقت الحالي.

ملاحظاتي الوحيدة هي أن type foo,bar داخل الواجهة يبدو محرجًا بعض الشيء ومن الدرجة الثانية ، وأنا أوافق على أنه يجب أن يكون هناك خيار بين nullable و non-nullable (إن أمكن).

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

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

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

علاوة على ذلك ، أنا أتفق بشدة مع دانيال الذي أغلق هذه القضية كخداع لهذه القضية. لا أفهم لماذا يقول andig أنهم يقترحون شيئًا مختلفًا. بقدر ما أستطيع فهم نص # 21154 ، فإنه يقترح بالضبط نفس الشيء الذي نناقشه هنا

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

أنا إنسان ويمكن أن يكون موضوع البستنة خادعًا في بعض الأحيان ، لذلك أشير بكل الوسائل عندما أرتكب خطأ :) ولكن في هذه الحالة أعتقد أن أي اقتراح لأنواع مجموع محددة يجب أن يتفرع من هذا الموضوع تمامًا مثل https: / /github.com/golang/go/issues/19412#issuecomment -701625548

أنا إنسان ويمكن أن يكون موضوع البستنة خادعًا في بعض الأحيان ، لذلك أشير بكل الوسائل عندما أرتكب خطأ :) ولكن في هذه الحالة أعتقد أن أي اقتراح لأنواع مجموع محددة يجب أن يتفرع من هذا الموضوع تمامًا مثل

mvdan ليس بشريا. صدقني. أنا جاره. أنا فقط أمزح.

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

القصد الأصلي هو السماح بتجميع الأنواع حسب ارتباطها بمجالها ، حيث لا تشترك بالضرورة في السلوكيات المشتركة ، ويطلب من المترجم أن يفرض ذلك. في رأيي ، هذه مجرد مشكلة تحقق ثابتة ، تتم أثناء التجميع. ليست هناك حاجة للمترجم لإنشاء رمز يحتفظ بالعلاقة المعقدة بين الأنواع. قد تتعامل الشفرة التي تم إنشاؤها مع أنواع المجالات هذه بشكل طبيعي كما لو كانت من نوع الواجهة العادية {}. الفرق هو أن المترجم يقوم الآن بفحص نوع ثابت إضافي عند التجميع. هذا هو جوهر اقتراحي رقم 21154

henryas عظيم أن أراك! 😊
أتساءل عما إذا كان Golang لم يستخدم الكتابة بطة من شأنه أن يجعل العلاقة بين الأنواع أكثر صرامة ويسمح بتجميع الكائنات حسب صلة المجال الخاصة بهم كما وصفت في اقتراحك.

henryas عظيم أن أراك! 😊
أتساءل عما إذا كان Golang لم يستخدم الكتابة بطة من شأنه أن يجعل العلاقة بين الأنواع أكثر صرامة ويسمح بتجميع الكائنات حسب صلة المجال الخاصة بهم كما وصفت في اقتراحك.

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

henryas أوافق. لقد كان سؤالا افتراضيا. من المؤكد أن صانعي المحتوى قد فكروا بعمق في كل التقلبات.
من ناحية أخرى ، لن يظهر دليل الترميز مثل التحقق من توافق الواجهة أبدًا.
https://github.com/uber-go/guide/blob/master/style.md#verify -interface- الامتثال

هل يمكنك إجراء هذه المناقشة الخارجة عن الموضوع في مكان آخر من فضلك؟ هناك الكثير من الأشخاص المشتركين في هذه المشكلة.
كان رضا الواجهة المفتوحة جزءًا من Go منذ بدايتها ولن يتغير.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات