From ffd2ac7e7e3b265337e046453202779d5a339937 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 15 Jan 2026 19:40:30 +0100 Subject: [PATCH 1/3] [Foundation] Improve NSArray.From[NS]Objects slightly. * Enable nullability. * Reuse existing code to create the native array. * Add tests. * Add xml docs. * Update nullability in some of the consumers of these methods whenever it makes sense. * Add a 'nint count' overload to a few methods that takes 'int count', so that we use 'nint' for 'count' everywhere, and deprecated the 'int count' overloads. Contributes towards https://github.com/dotnet/macios/issues/17285. --- src/AudioUnit/AudioComponent.cs | 2 +- src/Foundation/NSArray.cs | 281 ++++++++++++------ src/Foundation/NSDictionary.cs | 10 +- src/Foundation/NSDictionary_2.cs | 6 +- src/Foundation/NSMutableDictionary.cs | 4 +- src/Foundation/NSMutableDictionary_2.cs | 12 +- src/Foundation/NSMutableSet.cs | 4 +- src/Foundation/NSMutableSet_1.cs | 6 +- src/Foundation/NSOrderedSet.cs | 6 +- .../Documentation.KnownFailures.txt | 2 - .../cecil-tests/HandleSafety.KnownFailures.cs | 2 +- .../monotouch-test/Foundation/NSArray1Test.cs | 274 +++++++++++++++++ .../Foundation/NSMutableOrderedSetTest.cs | 21 ++ .../Foundation/NSMutableSetTest.cs | 21 ++ .../Foundation/NSOrderedSetTest.cs | 45 +++ 15 files changed, 568 insertions(+), 128 deletions(-) diff --git a/src/AudioUnit/AudioComponent.cs b/src/AudioUnit/AudioComponent.cs index 03ec43c23177..741ccb1eda9b 100644 --- a/src/AudioUnit/AudioComponent.cs +++ b/src/AudioUnit/AudioComponent.cs @@ -675,7 +675,7 @@ public AudioComponentInfo []? ComponentList { } set { using var nameHandle = new TransientCFString (Name); - using var array = NSArray.FromNSObjects (h => h.Dictionary, value); + using var array = NSArray.FromNSObjects (h => h?.Dictionary, value); var result = (AudioConverterError) AudioUnitExtensionSetComponentList (nameHandle, array.GetHandle ()); switch (result) { case AudioConverterError.None: diff --git a/src/Foundation/NSArray.cs b/src/Foundation/NSArray.cs index 1bcc1734ab46..5d6b965e8768 100644 --- a/src/Foundation/NSArray.cs +++ b/src/Foundation/NSArray.cs @@ -25,6 +25,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using CoreFoundation; @@ -39,64 +40,87 @@ namespace Foundation { public partial class NSArray : IEnumerable { - // - // Creates an array with the elements; If the value passed is null, it - // still creates an NSArray object, but the Handle is set to IntPtr.Zero, - // this is so it makes it simpler for the generator to support - // [NullAllowed] on array parameters. - // - /// Strongly typed array of NSObjects. - /// Creates an NSArray from a C# array of NSObjects. - /// - /// - /// - /// - static public NSArray FromNSObjects (params NSObject [] items) +#nullable enable + /// Creates an NSArray from a C# array of NSObjects. + /// Strongly typed array of NSObjects. Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the specified objects. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (params NSObject? []? items) { return FromNativeObjects (items); } +#if !XAMCORE_5_0 + /// Creates an NSArray from the first elements of a C# array of NSObjects. /// Number of items to copy from the items array. - /// Strongly typed array of NSObjects. - /// Creates an NSArray from a C# array of NSObjects. - /// - /// - /// - /// - static public NSArray FromNSObjects (int count, params NSObject [] items) + /// Strongly typed array of NSObjects. Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the first objects from the array. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (int count, params NSObject? []? items) { return FromNativeObjects (items, count); } +#endif - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public static NSArray FromNSObjects (params INativeObject [] items) + /// Creates an NSArray from the first elements of a C# array of NSObjects. + /// Number of items to copy from the items array. + /// Strongly typed array of NSObjects. Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the first objects from the array. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (nint count, params NSObject? []? items) + { + return FromNativeObjects (items, count); + } + + /// Creates an NSArray from a C# array of objects implementing . + /// Array of objects implementing . Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the specified objects. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (params INativeObject? []? items) { return FromNativeObjects (items); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public static NSArray FromNSObjects (int count, params INativeObject [] items) +#if !XAMCORE_5_0 + /// Creates an NSArray from a C# array of objects implementing , using the first elements. + /// Number of items to copy from the items array. + /// Array of objects implementing . Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the first objects from the array. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (int count, params INativeObject? []? items) { return FromNativeObjects (items, count); } +#endif - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public static NSArray FromNSObjects (params T [] items) where T : class, INativeObject + /// Creates an NSArray from a C# array of objects implementing , using the first elements. + /// Number of items to copy from the items array. + /// Array of objects implementing . Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the first objects from the array. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (nint count, params INativeObject? []? items) + { + return FromNativeObjects (items, count); + } + + /// Creates an NSArray from a C# array of objects implementing . + /// The type of objects in the array, which must implement . + /// Array of objects. Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the specified objects. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (params T? []? items) where T : class, INativeObject { return FromNativeObjects (items); } - public static NSArray FromNSObjects (params T [] [] items) where T : class, INativeObject + + /// Creates a nested NSArray from a jagged array of objects implementing . + /// The type of objects in the array, which must implement . + /// A jagged array of objects. If null, returns null. + /// A new containing nested NSArrays for each row. Returns null if is null. + /// Thrown if any row or element in the jagged array is null. + /// Each row of the jagged array is converted to an NSArray, and these NSArrays are then stored in the returned NSArray. Individual row elements and rows themselves cannot be null. + [return: NotNullIfNotNull (nameof (items))] + public static NSArray? FromNSObjects (params T [] []? items) where T : class, INativeObject { if (items is null) return null; @@ -116,7 +140,14 @@ public static NSArray FromNSObjects (params T [] [] items) where T : class, I return ret; } - public static NSArray FromNSObjects (T [,] items) where T : class, INativeObject + + /// Creates a nested NSArray from a two-dimensional array of objects implementing . + /// The type of objects in the array, which must implement . + /// A two-dimensional array of objects. If null, returns null. + /// A new containing nested NSArrays, one for each row of the 2D array. Returns null if is null. + /// The two-dimensional array is converted to a jagged array structure where each row becomes a nested NSArray. + [return: NotNullIfNotNull (nameof (items))] + public static NSArray? FromNSObjects (T [,]? items) where T : class, INativeObject { if (items is null) return null; @@ -133,91 +164,137 @@ public static NSArray FromNSObjects (T [,] items) where T : class, INativeObj } return FromNSObjects (ret); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public static NSArray FromNSObjects (int count, params T [] items) where T : class, INativeObject + +#if !XAMCORE_5_0 + /// Creates an NSArray from a C# array of objects implementing , using the first elements. + /// The type of objects in the array, which must implement . + /// Number of items to copy from the items array. + /// Array of objects. Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the first objects from the array. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (int count, params T? []? items) where T : class, INativeObject { return FromNativeObjects (items, count); } +#endif - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public static NSArray FromNSObjects (Func nsobjectificator, params T [] items) + /// Creates an NSArray from a C# array of objects implementing , using the first elements. + /// The type of objects in the array, which must implement . + /// Number of items to copy from the items array. + /// Array of objects. Null elements are stored as . If the array itself is null, an empty is returned. + /// A new containing the first objects from the array. + /// Null items in the array are converted to instances. + public static NSArray FromNSObjects (nint count, params T? []? items) where T : class, INativeObject + { + return FromNativeObjects (items, count); + } + + /// Creates an NSArray from a C# array using a custom conversion function. + /// The type of objects in the input array. + /// A function that converts each item in the array to an . The function may return null. + /// Array of objects to convert. If null, returns null. + /// A new containing the converted objects. Returns null if is null. + /// Thrown if is null. + /// Each item is converted using the provided function. Null results from the conversion function are stored as in the resulting array. + [return: NotNullIfNotNull (nameof (items))] + public static NSArray? FromNSObjects (Func nsobjectificator, params T? []? items) + { + return FromNSObjects ((nint) (items?.Length ?? 0), nsobjectificator, items); + } + + /// Creates an NSArray from a C# array using a custom conversion function. + /// The type of objects in the input array. + /// Number of items to copy from the items array. + /// A function that converts each item in the array to an . The function may return null. + /// Array of objects to convert. If null, returns null. + /// A new containing the converted objects. Returns null if is null. + /// Thrown if is null. + /// Each item is converted using the provided function. Null results from the conversion function are stored as in the resulting array. + [return: NotNullIfNotNull (nameof (items))] + static NSArray? FromNSObjects (nint count, Func nsobjectificator, params T? []? items) { if (nsobjectificator is null) throw new ArgumentNullException (nameof (nsobjectificator)); + if (items is null) return null; - var arr = new NSObject [items.Length]; - for (int i = 0; i < items.Length; i++) { + if (count > items.Length) + throw new ArgumentException ("count is larger than the number of items", nameof (count)); + + if (count < 0) + throw new ArgumentOutOfRangeException (nameof (count), "count is negative"); + + if (count == 0) + return new NSArray (); + + var arr = new NSObject? [count]; + for (int i = 0; i < count; i++) { arr [i] = nsobjectificator (items [i]); } return FromNativeObjects (arr); } - /// Array of C# objects. - /// Creates an NSArray from a C# array of NSObjects. - /// - /// - /// The values will be boxed into - /// NSObjects using . - public static NSArray FromObjects (params object [] items) + /// Creates an NSArray from a C# array of objects. + /// Array of C# objects. Null elements will be boxed as . If the array itself is null, an empty is returned. + /// A new containing the boxed objects. + /// The values will be boxed into NSObjects using . Null items in the array are converted to instances. + public static NSArray FromObjects (params object? []? items) { - return From (items); + return FromObjects (items?.Length ?? 0, items); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public static NSArray FromObjects (nint count, params object [] items) + /// Creates an NSArray from the first elements of a C# array. + /// Number of items to copy from the items array. + /// Array of C# objects. Null elements will be boxed as . If the array itself is null, an empty is returned. + /// A new containing the first boxed objects from the array. + /// The values will be boxed into NSObjects using . Null items in the array are converted to instances. + public static NSArray FromObjects (nint count, params object? []? items) { return From (items, count); } - internal static NSArray From (T [] items, long count = -1) + internal static NSArray From (T? []? items) { - if ((items is null) || (count == 0)) - return new NSArray (); + return From (items, items?.Length ?? 0); + } - if (count == -1) - count = items.Length; - else if (count > items.Length) - throw new ArgumentException ("count is larger than the number of items", "count"); + internal static NSArray From (T? []? items, nint count) + { + if (items is null || count == 0) + return new NSArray (); - NSObject [] nsoa = new NSObject [count]; - for (nint i = 0; i < count; i++) { - var k = NSObject.FromObject (items [i]); + return FromNSObjects (count, (item) => { + var k = NSObject.FromObject (item); if (k is null) - throw new ArgumentException (String.Format ("Do not know how to marshal object of type '{0}' to an NSObject", items [i].GetType ())); - nsoa [i] = k; - } - return FromNSObjects (nsoa); + throw new ArgumentException (String.Format ("Do not know how to marshal object of type '{0}' to an NSObject", item?.GetType ())); + return k; + }, items); + } + + /// Creates an from an array of native objects. + /// The type of native objects in the array. + /// An array of objects implementing . If null, returns an empty . Any elements will throw an exception. + /// A new containing the specified objects. + /// This method creates a native NSArray from managed objects. An exception will be thrown if there are any elements. + internal static NSArray FromNonNullNativeObjects (T []? items) where T : class, INativeObject + { + return FromNativeObjectsImpl (items, items?.Length ?? 0, allowNullElements: false); } -#nullable enable /// Creates an from an array of native objects. /// The type of native objects in the array. /// An array of objects implementing . If null, returns an empty . /// A new containing the specified objects. Null items are represented as . - /// This method creates a native NSArray from managed objects. Null items in the array are converted to NSNull.Null instances. + /// + /// + /// This method creates a native NSArray from managed objects. Null items are represented as . + /// + /// internal static NSArray FromNativeObjects (T? []? items) where T : class, INativeObject { - if (items is null) - return new NSArray (); - - return FromNativeObjects (items, items.Length); + return FromNativeObjectsImpl (items, items?.Length ?? 0, allowNullElements: true); } /// Creates an from an array of native objects with a specified count. @@ -226,8 +303,13 @@ internal static NSArray FromNativeObjects (T? []? items) where T : class, INa /// The number of items from the array to include in the . /// A new containing the specified number of objects from the array. Null items are represented as . /// Thrown when is greater than the length of , or when is negative. - /// This method creates a native NSArray from the first elements of the managed array. Null items are converted to NSNull.Null instances. + /// This method creates a native NSArray from the first elements of the managed array. Null items are represented as . internal static NSArray FromNativeObjects (T? []? items, nint count) where T : class, INativeObject + { + return FromNativeObjectsImpl (items, count, allowNullElements: true); + } + + static NSArray FromNativeObjectsImpl (T? []? items, nint count, bool allowNullElements) where T : class, INativeObject { if (items is null) return new NSArray (); @@ -241,6 +323,8 @@ internal static NSArray FromNativeObjects (T? []? items, nint count) where T var handles = new IntPtr [count]; for (nint i = 0; i < count; i++) { var item = items [i]; + if (item is null && !allowNullElements) + throw new ArgumentNullException ($"{nameof (items)}[{i}]"); // The analyzer cannot deal with arrays, we manually keep alive the whole array below #pragma warning disable RBI0014 IntPtr h = item is null ? NSNull.Null.Handle : item.Handle; @@ -251,21 +335,22 @@ internal static NSArray FromNativeObjects (T? []? items, nint count) where T GC.KeepAlive (items); return rv; } -#nullable disable - internal static NSArray FromNSObjects (IList items) + internal static NSArray FromNSObjects (IList? items) { if (items is null) return new NSArray (); - int count = items.Count; - IntPtr buf = Marshal.AllocHGlobal (count * IntPtr.Size); + var count = items.Count; + var handles = new IntPtr [count]; for (int i = 0; i < count; i++) - Marshal.WriteIntPtr (buf, i * IntPtr.Size, items [i].Handle); - NSArray arr = Runtime.GetNSObject (NSArray.FromObjects (buf, count)); - Marshal.FreeHGlobal (buf); - return arr; + handles [i] = items [i].Handle; + + var rv = FromIntPtrs (handles); + GC.KeepAlive (items); + return rv; } +#nullable disable /// Array of C# strings. /// Creates an NSArray from a C# array of strings. diff --git a/src/Foundation/NSDictionary.cs b/src/Foundation/NSDictionary.cs index c4f4c3d1ec46..32c50e3e813f 100644 --- a/src/Foundation/NSDictionary.cs +++ b/src/Foundation/NSDictionary.cs @@ -51,7 +51,7 @@ public partial class NSDictionary : NSObject, IDictionary, IDictionary /// /// - public NSDictionary (NSObject first, NSObject second, params NSObject [] args) : this (PickOdd (second, args), PickEven (first, args)) + public NSDictionary (NSObject? first, NSObject? second, params NSObject? [] args) : this (PickOdd (second, args), PickEven (first, args)) { } @@ -87,21 +87,21 @@ internal NSDictionary (NativeHandle handle, bool owns) : base (handle, owns) { } - internal static NSArray PickEven (NSObject f, NSObject [] args) + internal static NSArray PickEven (NSObject? f, NSObject? [] args) { int al = args.Length; if ((al % 2) != 0) throw new ArgumentException ("The arguments to NSDictionary should be a multiple of two", "args"); - var ret = new NSObject [1 + al / 2]; + var ret = new NSObject? [1 + al / 2]; ret [0] = f; for (int i = 0, target = 1; i < al; i += 2) ret [target++] = args [i]; return NSArray.FromNSObjects (ret); } - internal static NSArray PickOdd (NSObject f, NSObject [] args) + internal static NSArray PickOdd (NSObject? f, NSObject? [] args) { - var ret = new NSObject [1 + args.Length / 2]; + var ret = new NSObject? [1 + args.Length / 2]; ret [0] = f; for (int i = 1, target = 1; i < args.Length; i += 2) ret [target++] = args [i]; diff --git a/src/Foundation/NSDictionary_2.cs b/src/Foundation/NSDictionary_2.cs index 23468bfae958..1e1a32a39b4b 100644 --- a/src/Foundation/NSDictionary_2.cs +++ b/src/Foundation/NSDictionary_2.cs @@ -117,7 +117,7 @@ internal static bool ValidateKeysAndValues (TKey [] keys, TValue [] values) return true; } - NSDictionary (TKey [] keys, TValue [] values, bool validation) + NSDictionary (TKey? [] keys, TValue? [] values, bool validation) : base (NSArray.FromNSObjects (values), NSArray.FromNSObjects (keys)) { } @@ -137,7 +137,7 @@ public NSDictionary (TKey [] keys, TValue [] values) /// /// The key. /// The value. - public NSDictionary (TKey key, TValue value) + public NSDictionary (TKey? key, TValue? value) : base (NSArray.FromNSObjects (value), NSArray.FromNSObjects (key)) { } @@ -231,7 +231,7 @@ public TValue [] ObjectsForKeys (TKey [] keys, TValue marker) return []; using (var pool = new NSAutoreleasePool ()) { - var keysArray = NSArray.From (keys); + var keysArray = NSArray.FromNativeObjects (keys); var result = NSArray.ArrayFromHandle (_ObjectsForKeys (keysArray.Handle, marker.Handle)); GC.KeepAlive (keysArray); GC.KeepAlive (marker); diff --git a/src/Foundation/NSMutableDictionary.cs b/src/Foundation/NSMutableDictionary.cs index a6587a32f2ea..3e57ae94e391 100644 --- a/src/Foundation/NSMutableDictionary.cs +++ b/src/Foundation/NSMutableDictionary.cs @@ -39,7 +39,7 @@ public partial class NSMutableDictionary : NSDictionary, IDictionary, IDictionar /// A new mutable dictionary containing the specified objects and keys. /// Thrown when or is . /// Thrown when the arrays have different sizes. - public static NSMutableDictionary FromObjectsAndKeys (NSObject [] objects, NSObject [] keys) + public static NSMutableDictionary FromObjectsAndKeys (NSObject? [] objects, NSObject? [] keys) { if (!ValidateFromObjectsAndKeys (objects, keys)) return new NSMutableDictionary (); @@ -68,7 +68,7 @@ public static NSMutableDictionary FromObjectsAndKeys (object [] objects, object /// A new mutable dictionary containing the specified objects and keys. /// Thrown when or is . /// Thrown when is invalid. - public static NSMutableDictionary FromObjectsAndKeys (NSObject [] objects, NSObject [] keys, nint count) + public static NSMutableDictionary FromObjectsAndKeys (NSObject? [] objects, NSObject? [] keys, nint count) { if (!ValidateFromObjectsAndKeys (objects, keys, count)) return new NSMutableDictionary (); diff --git a/src/Foundation/NSMutableDictionary_2.cs b/src/Foundation/NSMutableDictionary_2.cs index 473efef936af..702407d3034e 100644 --- a/src/Foundation/NSMutableDictionary_2.cs +++ b/src/Foundation/NSMutableDictionary_2.cs @@ -80,7 +80,7 @@ public NSMutableDictionary (NSDictionary other) { } - NSMutableDictionary (TKey [] keys, TValue [] values, bool validation) + NSMutableDictionary (TKey? [] keys, TValue? [] values, bool validation) : base (NSArray.FromNSObjects (values), NSArray.FromNSObjects (keys)) { } @@ -100,7 +100,7 @@ public NSMutableDictionary (TKey [] keys, TValue [] values) /// /// The key. /// The value. - public NSMutableDictionary (TKey key, TValue value) + public NSMutableDictionary (TKey? key, TValue? value) : base (NSArray.FromNSObjects (value), NSArray.FromNSObjects (key)) { } @@ -173,7 +173,7 @@ public TValue [] ObjectsForKeys (TKey [] keys, TValue marker) if (keys.Length == 0) return []; - var keysArray = NSArray.From (keys); + var keysArray = NSArray.FromNativeObjects (keys); var result = NSArray.ArrayFromHandle (_ObjectsForKeys (keysArray.Handle, marker.Handle)); GC.KeepAlive (keysArray); GC.KeepAlive (marker); @@ -263,7 +263,7 @@ public TValue? this [TKey index] { /// An array of keys. /// The number of elements to use from each array. /// A new mutable dictionary containing the specified key-value pairs. - public static NSMutableDictionary? FromObjectsAndKeys (TValue [] objects, TKey [] keys, nint count) + public static NSMutableDictionary? FromObjectsAndKeys (TValue? [] objects, TKey? [] keys, nint count) { if (!ValidateFromObjectsAndKeys (objects, keys, count)) return new NSMutableDictionary (); @@ -279,7 +279,7 @@ public TValue? this [TKey index] { /// An array of values. /// An array of keys. /// A new mutable dictionary containing the specified key-value pairs. - public static NSMutableDictionary? FromObjectsAndKeys (TValue [] objects, TKey [] keys) + public static NSMutableDictionary? FromObjectsAndKeys (TValue? [] objects, TKey? [] keys) { if (!ValidateFromObjectsAndKeys (objects, keys)) return new NSMutableDictionary (); @@ -308,7 +308,7 @@ public TValue? this [TKey index] { /// An array of keys. /// The number of elements to use from each array. /// A new mutable dictionary containing the specified key-value pairs. - public static NSMutableDictionary? FromObjectsAndKeys (NSObject [] objects, NSObject [] keys, nint count) + public static NSMutableDictionary? FromObjectsAndKeys (NSObject? [] objects, NSObject? [] keys, nint count) { if (!ValidateFromObjectsAndKeys (objects, keys, count)) return new NSMutableDictionary (); diff --git a/src/Foundation/NSMutableSet.cs b/src/Foundation/NSMutableSet.cs index 9f225b4488c8..122b3cee0911 100644 --- a/src/Foundation/NSMutableSet.cs +++ b/src/Foundation/NSMutableSet.cs @@ -38,7 +38,7 @@ namespace Foundation { public partial class NSMutableSet : IEnumerable { /// Initializes a new mutable set with the specified objects. /// The objects to add to the set. - public NSMutableSet (params NSObject [] objs) + public NSMutableSet (params NSObject? []? objs) : this (NSArray.FromNSObjects (objs)) { } @@ -50,7 +50,7 @@ public NSMutableSet (params string [] strings) { } - internal NSMutableSet (params INativeObject [] objs) + internal NSMutableSet (params INativeObject? []? objs) : this (NSArray.FromNSObjects (objs)) { } diff --git a/src/Foundation/NSMutableSet_1.cs b/src/Foundation/NSMutableSet_1.cs index 253d70014916..c3506ade3def 100644 --- a/src/Foundation/NSMutableSet_1.cs +++ b/src/Foundation/NSMutableSet_1.cs @@ -190,11 +190,7 @@ public void AddObjects (params TKey [] objects) if (objects is null) throw new ArgumentNullException (nameof (objects)); - for (int i = 0; i < objects.Length; i++) - if (objects [i] is null) - throw new ArgumentNullException (nameof (objects) + "[" + i.ToString () + "]"); - - using (var array = NSArray.From (objects)) + using (var array = NSArray.FromNonNullNativeObjects (objects)) _AddObjects (array.Handle); } diff --git a/src/Foundation/NSOrderedSet.cs b/src/Foundation/NSOrderedSet.cs index e24bf7965c16..35b984f2f2c3 100644 --- a/src/Foundation/NSOrderedSet.cs +++ b/src/Foundation/NSOrderedSet.cs @@ -39,7 +39,7 @@ public partial class NSOrderedSet : IEnumerable { /// Initializes a new instance of the class from an array of instances. /// An array of instances to include in the set. - public NSOrderedSet (params NSObject [] objs) : this (NSArray.FromNSObjects (objs)) + public NSOrderedSet (params NSObject? []? objs) : this (NSArray.FromNSObjects (objs)) { } @@ -77,7 +77,7 @@ public T [] ToArray () where T : class, INativeObject /// The type of values in the array, must be a class that derives from . /// An array of strongly typed values to include in the ordered set. /// A new containing the specified values. - public static NSOrderedSet MakeNSOrderedSet (T [] values) where T : NSObject + public static NSOrderedSet MakeNSOrderedSet (T? []? values) where T : NSObject { NSArray a = NSArray.FromNSObjects (values); var result = (NSOrderedSet) Runtime.GetNSObject (ObjCRuntime.Messaging.IntPtr_objc_msgSend_IntPtr (class_ptr, Selector.GetHandle (selSetWithArray), a.Handle))!; @@ -231,7 +231,7 @@ public bool Contains (object? obj) public partial class NSMutableOrderedSet { /// Initializes a new instance of the class from an array of instances. /// An array of instances to include in the set. - public NSMutableOrderedSet (params NSObject [] objs) : this (NSArray.FromNSObjects (objs)) + public NSMutableOrderedSet (params NSObject? []? objs) : this (NSArray.FromNSObjects (objs)) { } diff --git a/tests/cecil-tests/Documentation.KnownFailures.txt b/tests/cecil-tests/Documentation.KnownFailures.txt index c0bedcdbd785..4548fccb79b6 100644 --- a/tests/cecil-tests/Documentation.KnownFailures.txt +++ b/tests/cecil-tests/Documentation.KnownFailures.txt @@ -11806,8 +11806,6 @@ M:Foundation.INSUrlSessionWebSocketDelegate.DidOpen(Foundation.NSUrlSession,Foun M:Foundation.INSXpcListenerDelegate.ShouldAcceptConnection(Foundation.NSXpcListener,Foundation.NSXpcConnection) M:Foundation.NSArray.ArrayFromHandle``1(ObjCRuntime.NativeHandle,System.Converter{ObjCRuntime.NativeHandle,``0},System.Boolean) M:Foundation.NSArray.EnumsFromHandle``1(ObjCRuntime.NativeHandle) -M:Foundation.NSArray.FromNSObjects``1(``0[][]) -M:Foundation.NSArray.FromNSObjects``1(``0[0:,0:]) M:Foundation.NSArray.FromStrings(System.Collections.Generic.IReadOnlyList{System.String}) M:Foundation.NSArray.ToArray M:Foundation.NSArray.ToArray``1 diff --git a/tests/cecil-tests/HandleSafety.KnownFailures.cs b/tests/cecil-tests/HandleSafety.KnownFailures.cs index a3d5c99c14d1..a12e11a75242 100644 --- a/tests/cecil-tests/HandleSafety.KnownFailures.cs +++ b/tests/cecil-tests/HandleSafety.KnownFailures.cs @@ -105,7 +105,7 @@ public partial class HandleSafetyTest { "Foundation.DictionaryContainer.SetNativeValue (Foundation.NSString, ObjCRuntime.INativeObject, System.Boolean)", "Foundation.DictionaryContainer.TryGetNativeValue (ObjCRuntime.NativeHandle, ObjCRuntime.NativeHandle&)", "Foundation.DictionaryContainerHelper.GetHandle (Foundation.DictionaryContainer)", - "Foundation.NSArray.FromNativeObjects`1 (T[], System.IntPtr)", + "Foundation.NSArray.FromNativeObjectsImpl`1 (T[], System.IntPtr, System.Boolean)", "Foundation.NSArray.FromNSObjects (System.Collections.Generic.IList`1)", "Foundation.NSArray.FromStrings (System.Collections.Generic.IReadOnlyList`1)", "Foundation.NSArray.UnsafeGetItem (ObjCRuntime.NativeHandle, System.UIntPtr, System.Type)", diff --git a/tests/monotouch-test/Foundation/NSArray1Test.cs b/tests/monotouch-test/Foundation/NSArray1Test.cs index 3afb37a706a6..6691b54dcc1f 100644 --- a/tests/monotouch-test/Foundation/NSArray1Test.cs +++ b/tests/monotouch-test/Foundation/NSArray1Test.cs @@ -157,6 +157,280 @@ public void FromIntPtrs_NativeHandle_Empty () } } + [Test] + public void FromNSObjects_JaggedArray () + { + var str1 = (NSString) "1"; + var str2 = (NSString) "2"; + var str3 = (NSString) "3"; + var str4 = (NSString) "4"; + + var jaggedArray = new NSString [] [] { + new NSString [] { str1, str2 }, + new NSString [] { str3, str4 } + }; + + using (var arr = NSArray.FromNSObjects (jaggedArray)) { + Assert.AreEqual ((nuint) 2, arr.Count, "Outer array count"); + var row0 = arr.GetItem (0); + var row1 = arr.GetItem (1); + Assert.AreEqual ((nuint) 2, row0.Count, "Row 0 count"); + Assert.AreEqual ((nuint) 2, row1.Count, "Row 1 count"); + Assert.AreEqual ("1", row0.GetItem (0).ToString (), "Row 0, Item 0"); + Assert.AreEqual ("2", row0.GetItem (1).ToString (), "Row 0, Item 1"); + Assert.AreEqual ("3", row1.GetItem (0).ToString (), "Row 1, Item 0"); + Assert.AreEqual ("4", row1.GetItem (1).ToString (), "Row 1, Item 1"); + } + } + + [Test] + public void FromNSObjects_JaggedArray_Null () + { + NSString [] []? jaggedArray = null; + var arr = NSArray.FromNSObjects (jaggedArray); + Assert.IsNull (arr, "Should return null for null input"); + } + + [Test] + public void FromNSObjects_JaggedArray_NullRow () + { + var str1 = (NSString) "1"; + var jaggedArray = new NSString? [] [] { + new NSString [] { str1 }, + null + }; + + Assert.Throws (() => NSArray.FromNSObjects (jaggedArray), "Should throw for null row"); + } + + [Test] + public void FromNSObjects_JaggedArray_NullElement () + { + var str1 = (NSString) "1"; + var jaggedArray = new NSString? [] [] { + new NSString? [] { str1, null } + }; + + Assert.Throws (() => NSArray.FromNSObjects (jaggedArray), "Should throw for null element"); + } + + [Test] + public void FromNSObjects_2DArray () + { + var str1 = (NSString) "1"; + var str2 = (NSString) "2"; + var str3 = (NSString) "3"; + var str4 = (NSString) "4"; + + var array2D = new NSString [,] { + { str1, str2 }, + { str3, str4 } + }; + + using (var arr = NSArray.FromNSObjects (array2D)) { + Assert.AreEqual ((nuint) 2, arr.Count, "Outer array count"); + var row0 = arr.GetItem (0); + var row1 = arr.GetItem (1); + Assert.AreEqual ((nuint) 2, row0.Count, "Row 0 count"); + Assert.AreEqual ((nuint) 2, row1.Count, "Row 1 count"); + } + } + + [Test] + public void FromNSObjects_2DArray_Null () + { + NSString [,]? array2D = null; + var arr = NSArray.FromNSObjects (array2D); + Assert.IsNull (arr, "Should return null for null input"); + } + + [Test] + public void FromNSObjects_WithConverter () + { + var numbers = new int [] { 1, 2, 3 }; + using (var arr = NSArray.FromNSObjects (x => NSNumber.FromInt32 (x), numbers)) { + Assert.AreEqual ((nuint) 3, arr.Count, "Count"); + Assert.AreEqual (1, arr.GetItem (0).Int32Value, "Item 0"); + Assert.AreEqual (2, arr.GetItem (1).Int32Value, "Item 1"); + Assert.AreEqual (3, arr.GetItem (2).Int32Value, "Item 2"); + } + } + + [Test] + public void FromNSObjects_WithConverter_Null () + { + int []? numbers = null; + var arr = NSArray.FromNSObjects (x => NSNumber.FromInt32 (x), numbers); + Assert.IsNull (arr, "Should return null for null input"); + } + + [Test] + public void FromNSObjects_WithConverter_NullConverter () + { + var numbers = new int [] { 1, 2, 3 }; + Assert.Throws (() => NSArray.FromNSObjects (null, numbers), "Should throw for null converter"); + } + + [Test] + public void FromNSObjects_WithConverter_ReturnsNull () + { + var numbers = new int? [] { 1, null, 3 }; + var arr = NSArray.FromNSObjects (x => x.HasValue ? NSNumber.FromInt32 (x.Value) : null, numbers); + Assert.IsNotNull (arr, "Array should not be null"); + Assert.AreEqual ((nuint) 3, arr!.Count, "Count"); + + // Check if the array actually contains NSNull at index 1 + // Use reflection or try-catch to see what's there + try { + var item0 = arr.GetItem (0); + Assert.IsNotNull (item0, "Item 0 should not be null"); + Assert.AreEqual (1, item0.Int32Value, "Item 0"); + } catch (Exception ex) { + Assert.Fail ($"Item 0 failed: {ex.Message}"); + } + + // The converter returns null, so we expect NSNull in the array + // But GetItem might skip null items or return null + var count = arr.Count; + Assert.AreEqual ((nuint) 3, count, "Should have 3 items including null"); + + try { + var item2 = arr.GetItem (2); + Assert.IsNotNull (item2, "Item 2 should not be null"); + Assert.AreEqual (3, item2.Int32Value, "Item 2"); + } catch (Exception ex) { + Assert.Fail ($"Item 2 failed: {ex.Message}"); + } + + arr.Dispose (); + } + + [Test] + public void FromObjects_WithCount_ConvertsOnlyCount () + { + var items = new object [] { 1, 2, 3, 4, 5 }; + + using (var arr = NSArray.FromObjects (2, items)) { + // This should only convert the first 2 items + Assert.AreEqual ((nuint) 2, arr.Count, "Count should be 2"); + } + } + + [Test] + public void FromObjects_CountLargerThanArray () + { + var items = new object [] { 1, 2, 3 }; + Assert.Throws (() => NSArray.FromObjects (5, items), "Should throw when count > array length"); + } + + [Test] + public void FromObjects_ConverterThrows () + { + // FromObjects uses NSObject.FromObject which can throw for unsupported types + var items = new object? [] { new global::System.IO.MemoryStream () }; + Assert.Throws (() => NSArray.FromObjects (items), "Should throw for unmarshalable type"); + } + + [Test] + public void FromNSObjects_CountFirst_WithNull () + { + var str1 = (NSString) "1"; + var str2 = (NSString) "2"; + var str3 = (NSString) "3"; + var items = new NSString? [] { str1, null, str3, str1 }; + + using (var arr = NSArray.FromNSObjects (3, items)) { + Assert.AreEqual ((nuint) 3, arr.Count, "Count should include null"); + Assert.AreEqual (str1, arr.GetItem (0), "Item 0"); + // Item 1 is null, but GetItem may not retrieve it properly + // Just verify count is correct (3 items including the null) + Assert.AreEqual (str3, arr.GetItem (2), "Item 2"); + } + } + + [Test] + public void FromNSObjects_CountFirst_Basic () + { + var str1 = (NSString) "1"; + var str2 = (NSString) "2"; + var str3 = (NSString) "3"; + var str4 = (NSString) "4"; + var items = new NSString [] { str1, str2, str3, str4 }; + + using (var arr = NSArray.FromNSObjects (2, items)) { + Assert.AreEqual ((nuint) 2, arr.Count, "Count"); + Assert.AreEqual (str1, arr.GetItem (0), "Item 0"); + Assert.AreEqual (str2, arr.GetItem (1), "Item 1"); + } + } + + [Test] + public void FromNSObjects_CountFirst_NullArray () + { + NSString []? items = null; + using (var arr = NSArray.FromNSObjects (0, items)) { + Assert.AreEqual ((nuint) 0, arr.Count, "Null array should create empty array"); + } + } + + [Test] + public void FromObjects_BasicTypes () + { + var items = new object [] { 1, "hello", 3.14, true }; + using (var arr = NSArray.FromObjects (items)) { + Assert.AreEqual ((nuint) 4, arr.Count, "Count"); + Assert.AreEqual (1, arr.GetItem (0).Int32Value, "Item 0"); + Assert.AreEqual ("hello", arr.GetItem (1).ToString (), "Item 1"); + Assert.AreEqual (3.14, arr.GetItem (2).DoubleValue, 0.01, "Item 2"); + Assert.AreEqual (true, arr.GetItem (3).BoolValue, "Item 3"); + } + } + + [Test] + public void FromObjects_Null () + { + object []? items = null; + using (var arr = NSArray.FromObjects (items)) { + Assert.AreEqual ((nuint) 0, arr.Count, "Should return empty array for null input"); + } + } + + [Test] + public void FromObjects_WithCount () + { + var items = new object [] { 1, 2, 3, 4, 5 }; + using (var arr = NSArray.FromObjects (3, items)) { + Assert.AreEqual ((nuint) 3, arr.Count, "Count"); + Assert.AreEqual (1, arr.GetItem (0).Int32Value, "Item 0"); + Assert.AreEqual (2, arr.GetItem (1).Int32Value, "Item 1"); + Assert.AreEqual (3, arr.GetItem (2).Int32Value, "Item 2"); + } + } + + [Test] + public void FromObjects_WithCountZero () + { + var items = new object [] { 1, 2, 3 }; + using (var arr = NSArray.FromObjects (0, items)) { + Assert.AreEqual ((nuint) 0, arr.Count, "Count should be 0"); + } + } + + [Test] + public void FromObjects_WithNegativeCount () + { + var items = new object [] { 1, 2, 3 }; + Assert.Throws (() => NSArray.FromObjects (-1, items), "Should throw for negative count"); + } + + [Test] + public void FromObjects_WithCount_Null () + { + using (var arr = NSArray.FromObjects (0, null)) { + Assert.AreEqual ((nuint) 0, arr.Count, "Should return empty array"); + } + } + #if false // https://github.com/dotnet/macios/issues/15577 [Test] public void GetDifferenceFromArrayTest () diff --git a/tests/monotouch-test/Foundation/NSMutableOrderedSetTest.cs b/tests/monotouch-test/Foundation/NSMutableOrderedSetTest.cs index bc792331bb1e..50b4bc5a4f1f 100644 --- a/tests/monotouch-test/Foundation/NSMutableOrderedSetTest.cs +++ b/tests/monotouch-test/Foundation/NSMutableOrderedSetTest.cs @@ -93,5 +93,26 @@ public void OperatorDifferentTest () Assert.IsFalse (oSet.Equals (oSet2), "NSMutableOrderedSetTest Equals must be false"); } } + + [Test] + public void Ctor_WithNull () + { + var str1 = (NSString) "1"; + NSObject? nullObj = null; + using (var set = new NSMutableOrderedSet (str1, nullObj)) { + Assert.AreEqual (2, (int) set.Count, "Count should include null"); + Assert.AreEqual (str1, set [0], "First item"); + Assert.IsInstanceOf (set [1], "Second item should be NSNull"); + } + } + + [Test] + public void Ctor_NullArray () + { + NSObject []? objs = null; + using (var set = new NSMutableOrderedSet (objs)) { + Assert.AreEqual (0, (int) set.Count, "Null array should create empty set"); + } + } } } diff --git a/tests/monotouch-test/Foundation/NSMutableSetTest.cs b/tests/monotouch-test/Foundation/NSMutableSetTest.cs index e3050be3138c..cadef860d89e 100644 --- a/tests/monotouch-test/Foundation/NSMutableSetTest.cs +++ b/tests/monotouch-test/Foundation/NSMutableSetTest.cs @@ -286,5 +286,26 @@ public void OperatorSubtract_SecondIsSupersetOfFirst () Assert.AreEqual ((nuint) 0, result.Count, "Superset Count"); } } + + [Test] + public void Ctor_WithNull () + { + var str1 = (NSString) "1"; + NSObject? nullObj = null; + using (var set = new NSMutableSet (str1, nullObj)) { + Assert.AreEqual ((nuint) 2, set.Count, "Count should include null"); + Assert.IsTrue (set.Contains (str1), "Should contain string"); + Assert.IsTrue (set.Contains (NSNull.Null), "Should contain NSNull"); + } + } + + [Test] + public void Ctor_NullArray () + { + NSObject []? objs = null; + using (var set = new NSMutableSet (objs)) { + Assert.AreEqual ((nuint) 0, set.Count, "Null array should create empty set"); + } + } } } diff --git a/tests/monotouch-test/Foundation/NSOrderedSetTest.cs b/tests/monotouch-test/Foundation/NSOrderedSetTest.cs index 430d845736c1..b4ef7934b167 100644 --- a/tests/monotouch-test/Foundation/NSOrderedSetTest.cs +++ b/tests/monotouch-test/Foundation/NSOrderedSetTest.cs @@ -167,5 +167,50 @@ public void OperatorDifferentTest () Assert.IsFalse (oSet.Equals (oSet2), "NSOrderedSetTest Equals must be false"); } } + + [Test] + public void Ctor_WithNull () + { + var str1 = (NSString) "1"; + NSObject? nullObj = null; + using (var set = new NSOrderedSet (str1, nullObj)) { + Assert.AreEqual (2, (int) set.Count, "Count should include null"); + Assert.AreEqual (str1, set [0], "First item"); + Assert.IsInstanceOf (set [1], "Second item should be NSNull"); + } + } + + [Test] + public void Ctor_NullArray () + { + NSObject []? objs = null; + using (var set = new NSOrderedSet (objs)) { + Assert.AreEqual (0, (int) set.Count, "Null array should create empty set"); + } + } + + [Test] + public void MakeNSOrderedSet_WithNull () + { + var str1 = (NSString) "1"; + var str2 = (NSString) "2"; + var values = new NSString? [] { str1, null, str2 }; + using (var set = NSOrderedSet.MakeNSOrderedSet (values)) { + Assert.AreEqual (3, (int) set.Count, "Count should include null"); + Assert.AreEqual (str1, set [0], "First item"); + Assert.IsInstanceOf (set [1], "Second item should be NSNull"); + Assert.AreEqual (str2, set [2], "Third item"); + } + } + + [Test] + public void MakeNSOrderedSet_NullArray () + { + NSString []? values = null; + using (var set = NSOrderedSet.MakeNSOrderedSet (values)) { + Assert.IsNotNull (set, "Should create a set"); + Assert.AreEqual (0, (int) set.Count, "Null array should create empty set"); + } + } } } From 528c36409df43675219654947d5af8b7c1ce8a92 Mon Sep 17 00:00:00 2001 From: GitHub Actions Autoformatter Date: Wed, 28 Jan 2026 00:07:15 +0000 Subject: [PATCH 2/3] Auto-format source code --- tests/monotouch-test/Foundation/NSArray1Test.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/monotouch-test/Foundation/NSArray1Test.cs b/tests/monotouch-test/Foundation/NSArray1Test.cs index 6691b54dcc1f..f58ba7d86c41 100644 --- a/tests/monotouch-test/Foundation/NSArray1Test.cs +++ b/tests/monotouch-test/Foundation/NSArray1Test.cs @@ -278,7 +278,7 @@ public void FromNSObjects_WithConverter_ReturnsNull () var arr = NSArray.FromNSObjects (x => x.HasValue ? NSNumber.FromInt32 (x.Value) : null, numbers); Assert.IsNotNull (arr, "Array should not be null"); Assert.AreEqual ((nuint) 3, arr!.Count, "Count"); - + // Check if the array actually contains NSNull at index 1 // Use reflection or try-catch to see what's there try { @@ -288,12 +288,12 @@ public void FromNSObjects_WithConverter_ReturnsNull () } catch (Exception ex) { Assert.Fail ($"Item 0 failed: {ex.Message}"); } - + // The converter returns null, so we expect NSNull in the array // But GetItem might skip null items or return null var count = arr.Count; Assert.AreEqual ((nuint) 3, count, "Should have 3 items including null"); - + try { var item2 = arr.GetItem (2); Assert.IsNotNull (item2, "Item 2 should not be null"); @@ -301,7 +301,7 @@ public void FromNSObjects_WithConverter_ReturnsNull () } catch (Exception ex) { Assert.Fail ($"Item 2 failed: {ex.Message}"); } - + arr.Dispose (); } @@ -309,7 +309,7 @@ public void FromNSObjects_WithConverter_ReturnsNull () public void FromObjects_WithCount_ConvertsOnlyCount () { var items = new object [] { 1, 2, 3, 4, 5 }; - + using (var arr = NSArray.FromObjects (2, items)) { // This should only convert the first 2 items Assert.AreEqual ((nuint) 2, arr.Count, "Count should be 2"); From 8f066e88f1ae0a7adb1bb3dfc789a175cf981a8f Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 28 Jan 2026 09:54:02 +0100 Subject: [PATCH 3/3] Copilot suggestions. --- src/Foundation/NSArray.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Foundation/NSArray.cs b/src/Foundation/NSArray.cs index 5d6b965e8768..49a05bd2a180 100644 --- a/src/Foundation/NSArray.cs +++ b/src/Foundation/NSArray.cs @@ -56,6 +56,8 @@ public static NSArray FromNSObjects (params NSObject? []? items) /// Strongly typed array of NSObjects. Null elements are stored as . If the array itself is null, an empty is returned. /// A new containing the first objects from the array. /// Null items in the array are converted to instances. + [EditorBrowsable (EditorBrowsableState.Never)] + // Don't obsolete this method, because forcing the first parameter to a 'nint' to pick the good overload to avoid the obsolete warning makes the calling code ugly. public static NSArray FromNSObjects (int count, params NSObject? []? items) { return FromNativeObjects (items, count); @@ -87,6 +89,8 @@ public static NSArray FromNSObjects (params INativeObject? []? items) /// Array of objects implementing . Null elements are stored as . If the array itself is null, an empty is returned. /// A new containing the first objects from the array. /// Null items in the array are converted to instances. + [EditorBrowsable (EditorBrowsableState.Never)] + // Don't obsolete this method, because forcing the first parameter to a 'nint' to pick the good overload to avoid the obsolete warning makes the calling code ugly. public static NSArray FromNSObjects (int count, params INativeObject? []? items) { return FromNativeObjects (items, count); @@ -172,6 +176,8 @@ public static NSArray FromNSObjects (params T? []? items) where T : class, IN /// Array of objects. Null elements are stored as . If the array itself is null, an empty is returned. /// A new containing the first objects from the array. /// Null items in the array are converted to instances. + [EditorBrowsable (EditorBrowsableState.Never)] + // Don't obsolete this method, because forcing the first parameter to a 'nint' to pick the good overload to avoid the obsolete warning makes the calling code ugly. public static NSArray FromNSObjects (int count, params T? []? items) where T : class, INativeObject { return FromNativeObjects (items, count);