عنوان:

‫چرا پیاده‌سازی Setter نسبت به Reflection الویت دارد؟


نویسنده: هادی مزارعی
تاریخ: ۱۴۰۴/۰۲/۲۲ ۱۶:۳۳
آدرس: www.dntips.ir
سلام

  1. قراره یک فایل اکسل شامل 300،000 ردیف و 170 ستون رو در بانک اطلاعاتی ذخیره کنم.
  2. یک مدل ایجاد کردم و Propertyها با اسامی معنادار تعریف شدند.
  3. فایل اکسل از منابع مختلفی دریافت می‌شوند و ممکن است نام ستون‌های اکسل (ردیف اول header می باشد) همنام و متناظر با نام Propertyها در مدل نباشند.
  4. جهت ایجاد ارتباط بین Propertyها و نام ستون‌ها در اکسل، یکبار تناظر توسط کاربر انجام می‌شود (یک سازوکار ساده و روان طراحی شده) و این تنظیمات ذخیره می‌شوند.
  5. زمان خواندن فایل اکسل بصورت Stream، هر ردیف اکسل در Propertyهای مدل با استفاده از Reflection نگاشت می‌شوند (تا اینجای کار همه چیز کار میکند - به عنوان روش اول).
  6. عملیاتی که در خط قبلی انجام شده، باید برای 300،000 ردیف دیگر نیز انجام شود.
  7. نمونه کد را جهت ارزیابی به ChatGPT دادم و با کمی اصلاح، نتیجه تقریبا مشابه بود.
  8. نمونه کد را به DeepSeek دادم و راهکار DeepSeek ایجاد یک Setter برای تمام Proeprtyها بود که نمونه کد پیشنهاد شده بصورت زیر می باشد:

private List<(string ColumnName, Action<ImportModel, string> PropertySetter)> 
        CreatePropertyMappings(IEnumerable<MatchColumn> matchColumns)
    {
        // Pre-compile property setters for better performance
        return matchColumns.Select(mc =>
        {
            var property = typeof(ImportModel).GetProperty(mc.PropertyName);
            if (property == null)
                throw new InvalidOperationException($"Property {mc.PropertyName} not found in ImportModel");

            // Create a compiled setter action
            Action<ImportModel, string> setter = (model, value) => 
            {
                try
                {
                    property.SetValue(model, value);
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException(
                        $"Error setting value '{value}' to property {mc.PropertyName}", ex);
                }
            };

            return (mc.ColumnName, setter);
        }).ToList();
    }

سوال: در هر دو پاسخ از Reflection استفاده شده ولی در دومی منتج به ایجاد یک Setter سفارشی شده و ایجاد Setter باید در هر مرحله از حلقه اجرا شود.

  1. آیا این کار سربار اضافه دارد؟ یا اینکه اگر سربار اضافه دارد مزیت فنی این موضوع در چیست؟
  2. آیا امکان دارد که یکبار برای مدل Setter ایجاد کرد و برای تمام 300،000 ردیف از همان مدل اولیه استفاده کرد و برنامه مجبور به ایجاد Setter برای 170 پروپرتی در 300،000 ردیف نباشد (تعداد کل Setterهای ایجاد شده 51،000،000)؟

تشکر

نظرات

  • هادی مزارعی در ۱۴۰۴/۰۲/۲۲ ۱۶:۴۳
    در واقع یک سیستم Dynamic ایجاد کردم که تولید کننده اکسل بتونه با توجه به الزامات خودش فایل اکسل را تولید کنه و محدودیتی از نظر نامگذاری ستون و توالی ستون‌ها نداشته باشه. ضمن اینکه ویژگی Import برای انواع فایل طراحی شده و اکثر متدها Generic هستند ولی مدل‌های ورود اطلاعات جهت ارسال به Handler جهت ذخیره در بانک اطلاعاتی از قبل طراحی شدند.
  • وحید نصیری در ۱۴۰۴/۰۲/۲۲ ۱۹:۳۳
    در ادامه، این کد فرضی که از کدهای شما استفاده می‌کند:
    private List<(string ColumnName, Action<ImportModel, string> PropertySetter)> propertyMappings;
    
    public void ProcessExcelRows(IEnumerable<Dictionary<string, string>> excelRows)
    {
        // یک‌بار نگاشت‌ها را ایجاد می‌کنیم
        propertyMappings = CreatePropertyMappings(matchColumns);
    
        foreach (var row in excelRows)
        {
            var model = new ImportModel();
            foreach (var mapping in propertyMappings)
            {
                if (row.TryGetValue(mapping.ColumnName, out var value))
                {
                    mapping.PropertySetter(model, value); // استفاده از Setter بدون Reflection
                }
            }
            // ذخیره مدل در دیتابیس یا پردازش بعدی
        }
    }
    این نکات لحاظ شده‌اند:
    - Setterها فقط یک‌بار در تابع CreatePropertyMappings ایجاد می‌شوند.
    - این Setterها در یک لیست ذخیره می‌شوند و برای تمام ردیف‌ها (300,000 ردیف) قابل‌ استفاده‌ی مجدد هستند.
    - بنابراین، شما 51,000,000 Setter ایجاد نمی‌کنید! فقط 170 Setter (یکی برای هر ستون) یک‌بار ایجاد می‌شود و برای تمام ردیف‌ها استفاده می‌شود.

    به زبان دیگر:
    - متد CreatePropertyMappings فقط یک‌بار اجرا می‌شود.
    - برای هر ردیف، از Setterهای موجود در propertyMappings استفاده می‌شود.
    - هیچ Setter جدیدی در حلقه 300,000 ردیف ایجاد نمی‌شود.

    نکته‌ی مهم این کد، استفاده از Action موجود در آن است که یک Setter از پیش آماده شده را ارائه می‌دهد. این Setterها در یک لیست ذخیره می‌شوند و در زمان پردازش هر ردیف، به‌جای استفاده از چندین مرحله Reflection، از این Setterهای آماده استفاده می‌شود. یعنی از Reflection فقط یک‌بار (در زمان ایجاد Setterها) استفاده می‌شود و برای 300,000 ردیف دیگر، نیازی به Reflection نیست.

    البته داخل این Action، هنوز از property.SetValue (یعنی Reflection) استفاده می‌کند، بنابراین هنوز بهینه نیست! روش بهینه سازی آن، استفاده از expression trees است.Expression Trees به شما امکان می‌دهد تا کدی پویا بسازید و آن را به‌صورت کامپایل‌شده اجرا کنید؛ چیزی شبیه به این:
    private List<(string ColumnName, Action<ImportModel, string> PropertySetter)> 
        CreatePropertyMappings(IEnumerable<MatchColumn> matchColumns)
    {
        return matchColumns.Select(mc =>
        {
            var property = typeof(ImportModel).GetProperty(mc.PropertyName);
            if (property == null)
                throw new InvalidOperationException($"Property {mc.PropertyName} not found in ImportModel");
    
            // ایجاد Expression Tree برای Setter
            var modelParam = Expression.Parameter(typeof(ImportModel), "model");
            var valueParam = Expression.Parameter(typeof(string), "value");
            var assign = Expression.Assign(
                Expression.Property(modelParam, property), // دسترسی مستقیم به Property
                valueParam // مقدار ورودی
            );
            var lambda = Expression.Lambda<Action<ImportModel, string>>(assign, modelParam, valueParam);
            var setter = lambda.Compile(); // کامپایل به کد اجرایی
    
            return (mc.ColumnName, setter);
        }).ToList();
    }
    چون این کد از پیش کامپایل شده‌است، برای هر ردیف اکسل (300,000 بار) به‌سرعت اجرا می‌شود.