Skip to content

Design question: raw array parameters (T[]/T[][]) can visually imply mutation without clear signalling #2036

Description

@shimat

Background

Public methods often accept raw arrays (T[], T[][]) for what are conceptually read-only inputs. In general C#, taking an array parameter doesn't by itself imply the callee will write into it. However, in this codebase there are real cases where the native interop layer does mutate the caller's array in place — via pinning (fixed) or [In, Out] P/Invoke marshaling. Because both the "pure input" and "mutated in place" patterns look identical at the call site (T[]), callers can't tell without reading the implementation whether an array argument is safe to reuse afterward or has been silently rewritten.

Concrete examples found

  1. Cv2.Find4QuadCornerSubpix(InputArray img, Point2f[] corners, Size regionSize) (src/OpenCvSharp/Cv2/Cv2_objdetect.cs:214) — copies the native result back into the caller's corners array element-by-element. This mutation is undocumented in the XML doc comment.
  2. Cv2.StereoCalibrate(..., double[] distCoeffs1, double[] distCoeffs2, ...) (src/OpenCvSharp/Cv2/Cv2_calib.cs:641) — pins distCoeffs1/distCoeffs2 via fixed and native writes back through the pointer. Correctly documented as "Input/output vector of distortion coefficients" (src/OpenCvSharp/Cv2/Cv2_calib.cs:629).
  3. Cv2.CalibrateCamera(..., double[] distCoeffs, ...) (src/OpenCvSharp/Cv2/Cv2_calib.cs:311) — same mutation mechanism as above (fixed pointer, native writes back), but documented as "Output vector of distortion coefficients" (src/OpenCvSharp/Cv2/Cv2_calib.cs:302), omitting that it is also an input.

Discussion so far

While working on #2013 (PR #2035), we discussed whether public methods should prefer IEnumerable<T>/IReadOnlyList<T> over raw arrays for pure inputs, to avoid this ambiguity. Conclusions reached for that PR's scope:

  • For 2D jagged inputs (T[][]) pinned via ArrayAddress2<T> for a native call's duration, keeping T[][] is preferable (avoids an allocation from converting IEnumerable<IEnumerable<T>>, and matches existing convention).
  • For 1D inputs, IEnumerable<T> removes the ambiguity for genuinely non-mutating cases — but when a 1D array parameter sits next to a 2D array parameter in the same signature (e.g. RefineDetectedMarkers(Point2f[][] detectedCorners, int[] detectedIds, Point2f[][] rejectedCorners, ...)), keeping it as T[] for visual/API consistency was judged preferable to introducing a lone IEnumerable<T> in the middle.

Open questions

  • Should we adopt (and document, e.g. in .github/copilot-instructions.md) an explicit convention distinguishing "pure input" arrays from "input that is mutated via native pinning/[In, Out] marshaling"?
  • Should mutating-array parameters be documented more consistently (fixing the CalibrateCamera/Find4QuadCornerSubpix doc gaps above), and should the codebase be audited for more undocumented instances?
  • Is there a lightweight convention (consistent "Input/output" doc wording, explicit [In, Out] attributes rather than relying on default marshaling, etc.) that makes mutation visible to callers without introducing a wrapper type?

This is a design/documentation question, not a specific bug — no urgency, but worth having a place to converge on a convention.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions