以与PHP array_merge类似的方式合并两个对象

In PHP it's common practice to pass an array as options for a class, and then merge that array with a set another array that holds the defaults.

Something like this.

class MyObject
{
     private static $defaults = array('value'=>10);
     private $settings;

     public function Something(array $settings = array())
     {
          $this->settings = array_merge(static::defaults,$settings);
     }
}

You can do the something in JavaScript using jQuery or other libraries that introduce the merge function. These scripts let you take two Javascript objects and merge them together. Allowing you to use one as the defaults, and another to override those defaults.

I've found this pattern very useful, because it allows you to configure a large set of defaults but only assign the settings you need.

Is there anyway to do something like this in C#?

I could write a function that uses reflection to do this on public properties, but I was thinking something like this must have already been done.

EDIT:

This question has been asked before on stack, but not answered in a way that provides the same simplicity as what can be done in PHP and Javascript.

I wasn't able to find an answer that did exactly what I wanted. So I wrote a small method to do this. It'll take two objects of the same time, and merge their fields/properties assuming that a null value represents an unassigned field/property.

Here is the usage example. Create a class to hold options for a communications class, let the communication class have defaults and then initialize the communication with user settings.

An example settings class.

public class ComSettings
{
    public int? Port;
    public string? Address;
    public bool? KeepAlive;
}

An example class that uses those settings in the constructor.

public class ComLibrary
{
    private static ComSettings _defaults = new ComSettings { Port = 80, Address = "localhost" };

    protected ComSettings settings;

    public ComLibrary(ComSettings pSettings)
    {
        this.settings = ObjectMerge<ComSettings>(_defaults, pSettings);
    }
}

This will let different classes use the ComSettings but each could have different defaults. The only limitation is that the field/properties have to support null assignments.

Here's the implementation of ObjectMerge.

    /// <summary>
    /// Creates a new object that contains the properties of the two objects merged together.
    /// </summary>
    /// <typeparam name="T">The class type to merge.</typeparam>
    /// <param name="pDefaults">Instance of the defaults object.</param>
    /// <param name="pSettings">Instance of the settings object.</param>
    /// <returns>A new instance of T with the merged results.</returns>
    public static T ObjectMerge<T>(T pDefaults, T pSettings, bool pMergeFields = true, bool pMergeProperties = true) where T : class, new()
    {
        T target = new T();
        Type type = typeof(T);
        List<MemberInfo> infos = new List<MemberInfo>(type.GetMembers());

        foreach (MemberInfo info in infos)
        {
            // Copy values from either defaults or settings
            if (pMergeFields && info.MemberType == MemberTypes.Field)
            {
                FieldInfo field = (FieldInfo)info;
                if (field.IsPublic)
                {
                    object value = field.GetValue(pSettings);
                    value = (value == null) ? field.GetValue(pDefaults) : value;
                    field.SetValue(target, value);
                }
            }

            // Copy values from either defaults or settings
            if (pMergeProperties && info.MemberType == MemberTypes.Property)
            {
                PropertyInfo prop = (PropertyInfo)info;
                if (prop.CanWrite && prop.CanRead)
                {
                    object value = prop.GetValue(pSettings, null);
                    value = (value == null) ? prop.GetValue(pDefaults, null) : value;
                    prop.SetValue(target, value, null);
                }
            }
        }

        return target;
    }

And here is a simple unit test.

/// <summary>
///This is a test class for CoreUtilsTest and is intended
///to contain all CoreUtilsTest Unit Tests
///</summary>
[TestClass()]
public class CoreUtilsTest
{
    /// <summary>
    /// A class to perform testing on.
    /// </summary>
    public class MyClassA
    {
        public string Param1;
        public string Param2;
        public string Param3;
    }

    /// <summary>
    /// A class to perform testing on.
    /// </summary>
    public class MyClassB
    {
        private string _param1;

        public string Param1
        {
            get { return _param1; }
            set { _param1 = value; }
        }
        private string _param2;

        public string Param2
        {
            get { return _param2; }
            set { _param2 = value; }
        }
        private string _param3;

        public string Param3
        {
            get { return _param3; }
            set { _param3 = value; }
        }
    }

    /// <summary>
    ///A test for SetProperties
    ///</summary>
    [TestMethod()]
    public void Merging_Fields()
    {
        MyClassA defaults = new MyClassA { Param1 = "defaults" };
        MyClassA settings = new MyClassA { Param2 = "settings" };
        MyClassA results = CoreUtils.ObjectMerge<MyClassA>(defaults, settings);

        Assert.AreEqual("defaults", results.Param1);
        Assert.AreEqual("settings", results.Param2);
        Assert.AreEqual(null, results.Param3);
    }

    [TestMethod()]
    public void Merging_Properties()
    {
        MyClassB defaults = new MyClassB { Param1 = "defaults" };
        MyClassB settings = new MyClassB { Param2 = "settings" };
        MyClassB results = CoreUtils.ObjectMerge<MyClassB>(defaults, settings);

        Assert.AreEqual("defaults", results.Param1);
        Assert.AreEqual("settings", results.Param2);
        Assert.AreEqual(null, results.Param3);
    }

}