نماینده‌ها (Delegates) در C#

14 تیر 1396
درسنامه درس 17 از سری آموزش سی شارپ (C#)
csharp-deltegates

در فصل گذشته یکی از کاربردی‌ترین مفاهیم زبان برنامه‌نویسی #C‌ تحت عنوان interface را با یکدیگر مورد بررسی قرار داده و مثال‌های کاربردی در ارتباط با آن را شرح دادیم. حال در این بخش می‌خواهیم به یکی دیگر از مفاهیم برنامه‌نویسی تحت عنوان Delegate (در لغت به معنای نماینده) بپردازیم. با ما همراه باشید.

مقدمه

Delegate در زبان برنامه‌نویسی #C همانند اشاره‌گر‌ها در توابع C و ++C است. یک delegate معرف یک متغییر نوع مرجع (Reference Type Variable) بوده که ارجاع به یک متد را در خود ذخیره می‌کند و این ارجاع در طی اجرای برنامه ممکن است تغییر کند.

از نظر نوع دقیقا مشابه interfaceها است. Delegateها به خصوص برای پیاده‌سازی Event‌ها و متدهای call-back (بازگشتی) بسیار مفید می‌باشند. تمام delegateها به صورت implicit از کلاس System.Delegate مشتق شده‌اند. ممکن است این سوال برای شما پیش بیاید که چرا نیاز داریم که یک ارجاع به یک متد داشته باشیم؟ در پاسخ به این سوال باید بگوییم که با استفاده از این کار حداکثر انعطاف‌پذیری برای اجرای هرگونه عملکردی را در زمان اجرا بدست می‌آورید.

مزایای استفاده از delegateها

۱) یک نماینده یا delegate به نوع static (بدون ساخت شیء) یا nonStatic (همراه با ساخت شیء) توجهی نمی‌کند.

۲) مدیریت کردن عملیات‌ها و اجرای هر یک از آنها هنگام بروز یک رویداد مشخص

تعریف Delegate

برای تعریف Delegate در زبان برنامه‌نویسی #C‌ از الگو و ساختار زیر بهره‌مند می‌شویم:

{access-modifier} delegate {return-type} {name}([parameters]);

همانطور که ملاحظه می‌کنید در این ساختار شرح جزئیات به صورت زیر می‌باشد:

access modifier: به عنوان یک روش برای تعیین سطح دسترسی به کار گرفته می‌شوند.

delegate: کلمه‌ی کلیدی delegate برای تعریف آن.

return-type: نوع بازگشتی یک متد delegate رار مشخص می‌کند.

name: معرف نام یک متد delegate می‌باشد.

parameters: شامل پارامترهای delegate است.

حال در ادامه یک مثال بسیار ساده درباره delegate ارائه می‌دهیم:

public delegate int MyDelegate(int a, int b)

در واقع این عبارت بدین معنی‌ست که delegate موردنظر با نام MyDelegate به توابعی اشاره کند که خروجی آنها از نوع int بوده و دو پارامتر از نوع int دارد. یعنی تمام توابع و متدهایی که شبیه به یک delegate‌ باشند را می‌توان به آن delegate نسبت داد.

حال یک مثال کلی‌تر و در فضای نرم‌افزار visual studio خدمت شما عزیزان ارائه می‌دهیم تا مفهومی دقیق‌تر از delegateها در ذهن داشته باشید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    class Program
    {
        public delegate void myDelegate(int a, int b);
        static void Main(string[] args)
        {
            Console.ReadKey();
        }
        static void sum(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, sum: {2}", a, b, a+b);
        }
    }
}

همانطور که ملاحظه می‌کنید یک delegate به نام myDelegate با دو ورودی از نوع int و یک خروجی بازگشتی از نوع viod تعریف کرده‌ایم. توجه به این نکته ضروری ست که می‌توان delegate ها را در خارج از کلاس نیز تعریف کرد.

حال می‌خواهیم برای استفاده از یک delegate ابتدا باید یک متغییر delegate در طی برنامه تعریف کنیم. بنابراین دستور زیر را بعد از تابع main می‌نویسم:

 myDelegate mathDelegate = new myDelegate(sum);

توجه دارید که در این خط یک متغییر به نام mathDelegate از نوع myDelegate‌ ایجاد کرده و مقداری که داخل آرگومان به عنوان یک سازنده یا constructor به آن ارسال کرده‌ایم، یک تابعی ست که از نظر شکل ظاهری (نوع بازگشتی و تعداد و نوع آرگومان) مشابه myDelegate ست که در بیرون از تابع main تعریف کردیم. حال برای استفاده از این متغییر ابتدا دو متغییر پیش‍فرض به نام‌های a و b‌ در ابتدای برنامه تعریف کرده و سپس دستور mathDelegate را اجرا می‌کنیم. در نظر داشته باشید الان عبارت mathDelegate دقیقا مشابه متد sum در طی برنامه عمل می‌کند چون در تعریف فوق، یک delegate به نام mathDelegate ایجاد کرده که نماینده (معنی لغت delegate) تابع یا متد sum است. بنابراین دستورهای ما به صورت زیر خواهد بود:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    class Program
    {
        public delegate void myDelegate(int a, int b);
        static void Main(string[] args)
        {
            int a = 100, b = 300;
            
            // تعریف یک delegate
            myDelegate mathDelegate = new myDelegate(sum);

            mathDelegate(a, b);

            Console.ReadKey();
        }
        static void sum(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, sum: {2}", a, b, a+b);
        }
    }
}

یعنی ما دیگر با متد sum کاری نداری و با استفاده از یک اشاره‌گر متد همواره می‌توانیم به آنها اشاره کنیم. خروجی این دستور به صورت زیر است:

a: 100, b: 300, sum: 400

معنی اشاره‌گر به یک متد این است که اگر هر تغییری روی mathDelegate انجام شود، عینا آن تغییر روی متد sum‌ که mathDelegate به آن اشاره، دارد، اعمال می‌شود.

برای اینکه کمی این مثال را گسترش دهیم توابع دیگری به مجموعه‌ی کدهای خود اضافه می‌کنیم که ساختاری شبیه به myDelegate‌ دارند. بنابراین داریم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    class Program
    {
        public delegate void myDelegate(int a, int b);
        static void Main(string[] args)
        {
            int a = 100, b = 300;
            
            // تعریف یک delegate
            myDelegate mathDelegate = new myDelegate(sum);

            mathDelegate(a, b);

            Console.ReadKey();
        }
        static void sum(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, sum: {2}", a, b, a+b);
        }
        static void sub(int a , int b)
        {
            Console.WriteLine("a: {0}, b: {1}, subtract: {2}", a, b, a - b);

        }
        static void multipe(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, muliple: {2}", a, b, a * b);
        }
        static void max(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, max: {2}", a, b, a > b? a:b);
        }
        static void min(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, min: {2}", a, b, a < b ? a : b);
        }
    }
}

قبل از اینکه به توضیح این مثال بپردازیم باید شما عزیزان را در جریان بگذاریم که یک روش دیگر تعریف متغییر delegate وجود دارد و می‌توان انتساب را به گونه‌ای دیگر انجام داد که در کد زیر مشاهده می‌کنید:

//تعریف یک متغییر delegate
myDelegate mathDelegate = sum;

این دستور دقیقا مشابه دستور زیر است:

myDelegate mathDelegate = new myDelegate(sum)

همچنین توجه کنید که delegate ها برای هر دو توابع و متدهای static و غیر static کار می‌کند و برای این انواع هیچگونه تفاوتی قائل نمی‌شود.

همانطور که در جریان هستید متغییر mathDelegate‌ همانند تمام انواع متغییر در زبان برنامه‌نویسی می‌تواند مقداری را بپذیرید و سپس با اعمال مقادیر بعدی، مقدار قبلی خود را پاک و مقدار جدید را جایگزین کند. این دقیقا خاصیت متغییرهاست. حال برای اینکه چندین متد را به یک delegate انتساب دهیم می‌توانیم از علامت =+ استفاده کرده و متدها را به صورت پشت سر هم به یک delegate نسبت دهیم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    class Program
    {
        public delegate void myDelegate(int a, int b);
        static void Main(string[] args)
        {
            int a = 100, b = 300;

            // تعریف یک delegate
            //myDelegate mathDelegate = new myDelegate(sum);

            myDelegate mathDelegate = sum;
            mathDelegate += sub;
            mathDelegate += multipe;
            mathDelegate += max;
            mathDelegate += min;

            mathDelegate(a, b);

            Console.ReadKey();
        }
        static void sum(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, sum: {2}", a, b, a + b);
        }
        static void sub(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, subtract: {2}", a, b, a - b);

        }
        static void multipe(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, muliple: {2}", a, b, a * b);
        }
        static void max(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, max: {2}", a, b, a > b ? a : b);
        }
        static void min(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, min: {2}", a, b, a < b ? a : b);
        }
    }
}

یعنی در مثال فوق ۴ تابع را به صورت یکجا فراخوانی کرده‌ایم و خروجی آن به صورت زیر است:

a: 100, b: 300, sum: 400
a: 100, b: 300, subtract: -200
a: 100, b: 300, multiple: 3000
a: 100, b: 300, max: 400
a: 100, b: 300, min: 100

حال فرض کنید می‌خواهیم یکی از توابع را از لیست فوق حذف کنیم. این کار را با استفاده از علامت =- استفاده می‌کنیم. به مثال زیر توجه کنید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    class Program
    {
        public delegate void myDelegate(int a, int b);
        static void Main(string[] args)
        {
            int a = 100, b = 300;

            // تعریف یک delegate
            //myDelegate mathDelegate = new myDelegate(sum);

            myDelegate mathDelegate = sum;
            mathDelegate += sub;
            mathDelegate += multipe;
            mathDelegate += max;
            mathDelegate += min;

            mathDelegate(a, b);
            Console.WriteLine("Roxo.ir------ Remove one method from delegate ------ Roxo.ir \n");
            mathDelegate -= max;
            mathDelegate(a, b);

            Console.ReadKey();
        }
        static void sum(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, sum: {2}", a, b, a + b);
        }
        static void sub(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, subtract: {2}", a, b, a - b);

        }
        static void multipe(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, muliple: {2}", a, b, a * b);
        }
        static void max(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, max: {2}", a, b, a > b ? a : b);
        }
        static void min(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, min: {2}", a, b, a < b ? a : b);
        }
    }
}

در این صورت خروجی به صورت زیر خواهد بود:

a: 100, b: 300, sum: 400
a: 100, b: 300, subtract: -200
a: 100, b: 300, multiple: 3000
a: 100, b: 300, max: 400
a: 100, b: 300, min: 100

Roxo.ir------ Remove one method from delegate ------ Roxo.ir

a: 100, b: 300, sum: 400
a: 100, b: 300, subtract: -200
a: 100, b: 300, multiple: 3000
a: 100, b: 300, min: 100

همانطور که ملاحظه فرمودید یک متد یا تابع از لیست نماینده حذف شد.

حال شما فرض کنید یک delegate از نوع int داشته باشید و این نماینده به توابع و متدهایی اشاره دارد که از نظر شکل ظاهری دقیقا مشابه آن بوده و دارای خروجی بازگشتی از نوع int هستند. بنابراین اگر مشابه مثال فوق با استفاده از دستور =+ تمام متدها را فراخوانی کنیم. آخرین مقدار دقیقا مقدار بازگشتی آخرین متد است. برای تفهیم این موضوع در مثال زیر یک نماینده دیگر ایجاد کرده و نام آن را outDelegate می‌گذاریم. برای اینکه در جریان باشید که این نماینده را می‌توان بیرون از کلاس هم تعریف کرد، اینکار را انجام داده‌ایم و آن را خارج از کلاس Program تعریف کرده‌ایم. سپس سه متد دیگر به نام‌های average و maxMinAverage و همچنین multipleMaxMinAverage نوشته و آن را با استفاده از متغییرهای delegate فراخوانی می‌کنیم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    public delegate int outDelegate(int a, int b);
    class Program
    {
        public delegate void myDelegate(int a, int b);
        static void Main(string[] args)
        {
            int a = 100, b = 300;

            // تعریف یک delegate
            //myDelegate mathDelegate = new myDelegate(sum);

            myDelegate mathDelegate = sum;
            mathDelegate += sub;
            mathDelegate += multipe;
            mathDelegate += max;
            mathDelegate += min;

            mathDelegate(a, b);
            Console.WriteLine("Roxo.ir------ Remove one method from delegate ------ Roxo.ir \n");
            mathDelegate -= max;
            mathDelegate(a, b);

            Console.WriteLine("Roxo.ir------ Return type int delegate ------ Roxo.ir \n");
            outDelegate aveDelegate = average;
            aveDelegate += maxMinAverage;
            aveDelegate += multipleMaxMinAverage;
            int z = aveDelegate(a, b);

            Console.WriteLine("Last Method Calculate: {0}", z);

            Console.ReadKey();
        }
        static void sum(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, sum: {2}", a, b, a + b);
        }
        static void sub(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, subtract: {2}", a, b, a - b);

        }
        static void multipe(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, muliple: {2}", a, b, a * b);
        }
        static void max(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, max: {2}", a, b, a > b ? a : b);
        }
        static void min(int a, int b)
        {
            Console.WriteLine("a: {0}, b: {1}, min: {2}", a, b, a < b ? a : b);
        }
        static int average(int a, int b)
        {
            Console.WriteLine("Calling average method...");
            return (a+b)/2;
        }
        static int maxMinAverage(int a, int b)
        {
            Console.WriteLine("Calling maxMinAverage method...");
            int max = a > b ? a : b;
            int min = a < b ? a : b;
            return max + min / 2;
        }
        static int multipleMaxMinAverage(int a, int b)
        {
            Console.WriteLine("Calling multipleMaxMinAverage method...");
            int max = a > b ? a : b;
            int min = a < b ? a : b;
            return max * min;
        }
    }
}

بنابراین با اجرای این مثال متوجه شدید که آخرین متدی که به متغییر delegate انتساب داده‌شده است نمایش داده خواهد شد. خروجی این مثال به صورت زیر است:

a: 100, b: 300, sum: 400
a: 100, b: 300, subtract: -200
a: 100, b: 300, multiple: 3000
a: 100, b: 300, max: 400
a: 100, b: 300, min: 100

Roxo.ir------ Remove one method from delegate ------ Roxo.ir

a: 100, b: 300, sum: 400
a: 100, b: 300, subtract: -200
a: 100, b: 300, multiple: 3000
a: 100, b: 300, min: 100


Roxo.ir------ Return type int delegate ------ Roxo.ir
Calling average method...
Calling maxMinAverage method...
Calling multipleMaxMinAverage method...
Last Method Calculate: 3000

سوال: آیا می‌توان یک مقدار null را به یک نماینده اختصاص داده و در نهایت متد موردنظر را فراخوانی کرد؟
پاسخ: خیر تحت هیچ شرایطی نمی‌توان به عنوان مثال عبارت mathDelegate = null‌ را ایجاد کرده و سپس نماینده mathDelegate(a,b) را فراخوانی کنیم.

سوال: آیا می‌توان یک پارامتر ref یا out را به یک نماینده اختصاص داده و در نهایت متد موردنظر را فراخوانی کرد؟
پاسخ: در صورتیکه بخواهیم یک متد با پارامترهای ورودی ref یا out را با استفاده از یک نماینده مورد اشاره قرار دهیم. باید حتما و حتما پارامترهای آن نماینده نیز به صورت ref یا out باشند. و به هنگام فراخوانی آن نماینده باید از دستور mathDelegate(ref a, out b) استفاده کنیم

سوال: آیا می‌توان یک پارامتر را در delegate‌ای به صورت optional parameters تعریف کرد؟
پاسخ: بله برای اینکار کافیست مقدار پیشفرض هر پارامتر را درون تعریف یک delegate قرار دهیم. مثلا int a =10 و در نهایت به هنگام فراخوانی آن پارامتر می‌توان نوشت mathDelegate(b) در این حالت مقدار a به صورت پیشفرض با عدد ۱۰ به متدی که نماینده به آن اشاره می‌کند ارسال می‌شود.

همچنین می‌توان علاوه بر اینکه یک پارامتر را تعریف کرد بلکه مقدار پیش فرض موردنظر را نیز به هنگام فراخوانی یک نماینده درون آن قرار داد به نمونه ی زیر توجه کنید:

mathDelegate(a: 10, b: 30)

در این حالت مقادیر پیشفرض درون متدها قرار می‌گیرند.

توابع Anonymous یا بی‌نام و ارتباط آن با Delegate

گاها پیش می‌اید که می‌خواهیم یک تابع بینام را حین تعریف یک متغییر نماینده مورد استفاده قرار دهیم. برای اینکار ابتدا یک نماینده تعریف کرده و سپس به هنگام فراخوانی آن از متدی کلیدی به نام delegate استفاده کنیم. به مثال زیر توجه کنید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    class Program
    {
        public delegate int myDelegate(int a, int b);
        public delegate int anonymousDelegate(int a, int b);
        static void Main(string[] args)
        {
            int a = 00, b = 300;

            Console.WriteLine("Roxo.ir ------ Commmon use of delegate ------ Roxo.ir \n");

            myDelegate mathDelegate = sum;
            int result = mathDelegate(a, b);
            Console.WriteLine("a: {0}, b: {1}, sum: {2} \n", a, b, result);


            Console.WriteLine("Roxo.ir ------ Delegate with Anonymous Function ------ Roxo.ir \n");
            //define anonymous function with delegate
            anonymousDelegate newDelegate = delegate (int c, int d)
            {
                return c + d;
            };
            Console.WriteLine("Result of newDelegate: {0}", newDelegate(a, b));

            Console.ReadKey();
        }
        static int sum(int a, int b)
        {
            return a + b;
        }
    }
}

همانطور که ملاحظه می‌کنید برای نماینده‌ی anonymousDelegate یک تابع بی نام ایجاد کرده‌ایم که از کلمه‌ی کلیدی delegate برای تعریف آن بهره برده‌ایم و اگر مجموعه دستورهای فوق را اجرا کنید خروجی زیر برای شما نمایش داده خواهد شد:

Roxo.ir------ Common use of delegate ------ Roxo.ir

a: 100, b: 300, sum: 400

Roxo.ir------ Delegate with Anonymous Function ------ Roxo.ir

Result of newDelegate: 400

یعنی از یک تابع بی نام برای تعریف یک متد با استفاده از یک نماینده اقدام کرده‌ایم. اما محدودیتهایی به هنگام استفاده از توابع بی نام وجود دارد که در زیر به برخی از آنها اشاره می‌کنیم:

۱) از دستورهای پرشی در داخل تابع بی نام نمی‌توان استفاده کرد. این دستورها شامل: goto, break و continue. اگر از این دستورها استفاده کنید کامپایلر به شما خطا نمایش می‌دهد.

۲) در این توابع نمی‌توان از پارامترهای out و ref استفاده کرد.

عبارت لامبدا (Lambda Expression) و Delegateها

عبارت لامبدا یا Lambda Expression در ورژن ۳ زبان برنامه‌نویسی #C معرفی شد. در یک خط و به صورت خیلی خلاصه اگر بخواهیم مفهوم عبارت لامبدا را خدمت شما عزیزان ارائه دهیم: نوع دیگری از نوشتار متدها و توابع هستند که دستیابی به منابع را راحت تر می‌کنند.

در ادامه یک تابع بی نام با استفاده از روش عبارت لامبدا ایجاد می‌کنیم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoxoApplication
{
    class Program
    {
        public delegate int myDelegate(int a, int b);
        public delegate int anonymousDelegate(int a, int b);
        static void Main(string[] args)
        {
            int a = 00, b = 300;

            Console.WriteLine("Roxo.ir ------ Commmon use of delegate ------ Roxo.ir \n");

            myDelegate mathDelegate = sum;
            int result = mathDelegate(a, b);
            Console.WriteLine("a: {0}, b: {1}, sum: {2} \n", a, b, result);


            Console.WriteLine("Roxo.ir ------ Delegate with Anonymous Function ------ Roxo.ir \n");
            //define anonymous function with delegate
            anonymousDelegate newDelegate = delegate (int c, int d)
            {
                return c + d;
            };
            Console.WriteLine("Result of newDelegate: {0} \n", newDelegate(a, b));


            Console.WriteLine("Roxo.ir ------ Delegate with Lambda Expression ------ Roxo.ir \n");
            //define lambda expression with delegate
            myDelegate lamdaDelegate = (e, f) => { return e + f; };
            Console.WriteLine("Result of lambdaDelegate: {0} \n", lamdaDelegate(a, b));

            Console.ReadKey();
        }
        static int sum(int a, int b)
        {
            return a + b;
        }
    }
}

یعنی دستوری که به صورت عبارت لامبدا نوشته شده است دقیقا مشابه متد sum عمل می‌کند. در نهایت خروجی این مثال به صورت زیر خواهد بود:

Roxo.ir------ Common use of delegate ------ Roxo.ir

a: 100, b: 300, sum: 400

Roxo.ir------ Delegate with Anonymous Function ------ Roxo.ir

Result of newDelegate: 400

Roxo.ir------ Delegate with Lambda Expression ------ Roxo.ir

Result of lambdaDelegate: 400

بسیار عالی به شما تبریک می‌گوییم در این بخش به صورت کامل مباحث مربوط به نماینده‌ها را فرا گرفتید. توجه داشته باشید که این آموزش حاصل سال‌ها تجربه‌ی کاری متخصصین شرکت روکسو بوده که به صورت رایگان در اختیار شما عزیزان قرار گرفته است. لذا از بازنشر آن در دیگر وب سایتها بدون ذکر منبع و آدرس‌دهی مستقیم به صفحه خودداری کنید. در جلسه‌ی بعدی مبحث بسیار مهم Event (رویداد) در زبان برنامه‌نویسی #C را به شما عزیزان آموزش خواهیم داد. با ما همراه باشید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش سی شارپ (C#) توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما (4 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

متین دهقانی پور
10 اسفند 1398
عالی بود.توضیح ها خیلی کامل و قابل فهم برای من بود.متشکرم

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

esmail
29 مهر 1398
سلام آموزش ها عالی و کاربردی بودن؛ ای کاش بجای ترجمه کلمات از اصل انگلیسی اونها استفاده شده بود. ممنون :)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

Feri
29 بهمن 1397
تشکر بابت زحماتتون یک پیشنهاد دارم و اینکه کلمات خاص رو ترجمه نکنید مثل نماینده توی آموزشهاتون با تشکر

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

م خلیلیان
04 شهریور 1397
سلام بسیار روان و عالی و واضح توضیح داده شده تشکر

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.