Files
XRLib/Assets/Scripts/UVC/Data/Core/DataArray.cs
2025-08-18 13:11:39 +09:00

540 lines
21 KiB
C#

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace UVC.Data.Core
{
/// <summary>
/// DataObject 객체 컬렉션의 변경사항을 추적하는 데이터 배열 클래스
/// </summary>
public class DataArray : List<DataObject>, IDataObject
{
private bool isInPool = false;
/// <summary>
/// 이 객체가 객체 풀에 있는지 여부를 나타냅니다.
/// 중복 반환을 방지하기 위해 DataArrayPool에서 내부적으로 사용됩니다.
/// </summary>
internal bool IsInPool
{
get => isInPool;
set
{
isInPool = value;
foreach (var item in this)
{
item.IsInPool = value; // 내부 DataObject도 풀에 있다고 표시합니다.
}
}
}
private bool createdFromPool = false;
/// <summary>
/// 객체가 풀에서 생성되었는지 여부를 나타냅니다.
/// </summary>
internal bool CreatedFromPool
{
get => createdFromPool;
set
{
createdFromPool = value;
foreach (var item in this)
{
item.CreatedFromPool = value; // 내부 DataObject도 풀에서 생성되었음을 표시합니다.
}
}
}
// 추가 된 항목 목록
protected List<DataObject> addedList = new List<DataObject>();
// 제거 된 항목 목록
protected List<DataObject> removedList = new List<DataObject>();
// 수정 된 항목 목록
protected List<DataObject> modifiedList = new List<DataObject>();
// 추가된 항목에 접근할 수 있는 읽기 전용 컬렉션
public ReadOnlyCollection<DataObject> AddedItems => addedList.AsReadOnly();
// 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션
public ReadOnlyCollection<DataObject> RemovedItems => removedList.AsReadOnly();
// 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션
public ReadOnlyCollection<DataObject> ModifiedList => modifiedList.AsReadOnly();
/// <summary>
/// 기본 생성자
/// </summary>
public DataArray() : base()
{
}
/// <summary>
/// 초기 용량을 지정하는 생성자
/// </summary>
/// <param name="capacity">초기 용량</param>
public DataArray(int capacity) : base(capacity)
{
}
/// <summary>
/// 기존 컬렉션으로부터 생성하는 생성자
/// </summary>
/// <param name="collection">초기 항목을 포함하는 컬렉션</param>
public DataArray(IEnumerable<DataObject> collection) : base(collection)
{
}
public DataArray(string jsonString) : base()
{
if (!string.IsNullOrEmpty(jsonString))
{
try
{
JArray jArray = JArray.Parse(jsonString);
foreach (var item in jArray)
{
Add(ConvertToDataObject(item));
}
}
catch (Exception ex)
{
throw new ArgumentException("Invalid JSON string format.", nameof(jsonString), ex);
}
}
}
public DataArray(System.IO.Stream jsonStream)
{
if (jsonStream == null)
throw new ArgumentNullException(nameof(jsonStream));
// 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만,
// 현재는 Newtonsoft.Json의 기본 역직렬화 사용
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
{
// 청크 읽기 설정 - 메모리 사용량 최적화
reader.SupportMultipleContent = true;
var serializer = new Newtonsoft.Json.JsonSerializer();
var sourceObject = serializer.Deserialize<JArray>(reader);
// 수정된 코드: 생성자를 호출하는 대신 JArray 메서드를 사용
if (sourceObject != null) FromJArray(sourceObject);
}
}
/// <summary>
/// JArray로부터 DataArray를 생성하는 생성자
/// </summary>
/// <param name="jArray">JSON 배열</param>
public DataArray(JArray jArray) : base()
{
FromJArray(jArray);
}
/// <summary>
/// 배열의 JSON 문자열 표현을 사용하여 현재 <see cref="DataArray"/> 인스턴스를 채웁니다.
/// </summary>
/// <remarks>이 메서드는 제공된 JSON 문자열을 구문 분석하고 각 요소를 데이터 객체로 변환한 다음
/// 현재 <see cref="DataArray"/>에 추가합니다. <paramref name="jsonString"/>이 null이거나
/// 비어 있는 경우, 메서드는 수정 없이 현재 인스턴스를 반환합니다.</remarks>
/// <param name="jsonString">구문 분석하여 데이터 객체로 변환할 JSON 문자열입니다. 유효한 JSON 배열을 나타내야 합니다.</param>
/// <returns>JSON 문자열에서 파싱된 데이터 객체로 채워진 현재 <see cref="DataArray"/> 인스턴스입니다.</returns>
/// <exception cref="ArgumentException"> <paramref name="jsonString"/>이 유효한 JSON 배열 형식이 아닌 경우 발생합니다.</exception>
public DataArray FromJsonString(string jsonString)
{
if (!string.IsNullOrEmpty(jsonString))
{
try
{
JArray jArray = JArray.Parse(jsonString);
foreach (var item in jArray)
{
Add(ConvertToDataObject(item));
}
}
catch (Exception ex)
{
throw new ArgumentException("Invalid JSON string format.", nameof(jsonString), ex);
}
}
return this;
}
public DataArray FromCapacity(int capacity)
{
Capacity = capacity;
return this;
}
public DataArray FromCollection(IEnumerable<DataObject> collection)
{
if (collection == null) return this;
foreach (var item in collection)
{
// base.Add를 사용하여 추적 로직을 우회하고 직접 추가합니다.
base.Add(item);
}
return this;
}
/// <summary>
/// JArray로부터 DataArray의 내용을 채웁니다.
/// </summary>
/// <param name="jArray">JSON 배열</param>
public DataArray FromJArray(JArray jArray)
{
if (jArray == null) return this;
foreach (var item in jArray)
{
// base.Add를 사용하여 추적 로직을 우회하고 직접 추가합니다.
base.Add(ConvertToDataObject(item));
}
return this;
}
/// <summary>
/// JToken을 DataObject로 변환합니다.
/// </summary>
private DataObject ConvertToDataObject(JToken token)
{
if (token.Type == JTokenType.Object)
{
// 풀에서 객체를 가져와 JObject로 초기화합니다.
var dataObject = DataObjectPool.Get();
dataObject.FromJObject((JObject)token);
return dataObject;
}
else
{
// JObject가 아닌 경우, 풀에서 새 DataObject를 가져와 값을 넣어줍니다.
var dataObject = DataObjectPool.Get();
dataObject.Add("value", ConvertJTokenToObject(token));
return dataObject;
}
}
private object ConvertJTokenToObject(JToken token)
{
switch (token.Type)
{
case JTokenType.String:
return token.ToString();
case JTokenType.Integer:
return token.ToObject<int>();
case JTokenType.Float:
return token.ToObject<float>();
case JTokenType.Boolean:
return token.ToObject<bool>();
case JTokenType.Object:
var dataObject = DataObjectPool.Get();
dataObject.FromJObject((JObject)token);
return dataObject;
case JTokenType.Array:
JArray array = (JArray)token;
var dataArray = DataArrayPool.Get();
dataArray.FromJArray(array);
return dataArray;
default:
return token.ToString();
}
}
/// <summary>
/// 모든 아이템이 추가 된것으로 표시합니다.
/// 전체 데이터가 갱신되었을 때 사용합니다.
/// </summary>
public void MarkAllAsUpdated()
{
addedList.Clear();
addedList.AddRange(this);
}
/// <summary>
/// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다.
/// 변경된 키는 자동으로 추적됩니다.
/// </summary>
/// <param name="other">비교할 DataObject</param>
public void UpdateDifferent(IDataObject other)
{
if (other == null) return;
if (!(other is DataArray otherArray)) return;
// 기존 변경 추적 목록을 초기화합니다.
ClearTrackedChanges();
//Port에 서도 동일한 Id를 사용하는 경우가 있기 때문에
//GroupBy를 사용하여 Id로 그룹화한 후, 각 그룹의 첫 번째 항목을 선택하여 딕셔너리를 생성
var thisDict = this.GroupBy(item => item.Id).ToDictionary(g => g.Key, g => g.First());
var otherDict = otherArray.GroupBy(item => item.Id).ToDictionary(g => g.Key, g => g.First());
var thisIds = new HashSet<string>(thisDict.Keys);
var otherIds = new HashSet<string>(otherDict.Keys);
// 제거된 항목 확인 (현재 배열에는 있지만 다른 배열에는 없는 항목)
foreach (var id in thisIds.Where(id => !otherIds.Contains(id)))
{
removedList.Add(thisDict[id]);
}
// 추가된 항목 확인 (다른 배열에는 있지만 현재 배열에는 없는 항목)
foreach (var id in otherIds.Where(id => !thisIds.Contains(id)))
{
addedList.Add(otherDict[id].Copy(fromPool: false));
}
// 수정된 항목 확인 (양쪽 모두에 있지만 내용이 다른 항목)
foreach (var id in thisIds.Where(id => otherIds.Contains(id)))
{
var thisItem = thisDict[id];
var otherItem = otherDict[id];
// [성능 개선] ToString() 비교는 매우 비효율적입니다.
// DataObject.UpdateDifferent를 직접 호출하여 변경 사항을 적용하고,
// UpdatedCount를 통해 실제 변경 여부를 확인합니다.
thisItem.UpdateDifferent(otherItem);
if (thisItem.UpdatedCount > 0)
{
modifiedList.Add(thisItem);
}
}
// [성능 개선] RemoveAll과 HashSet을 사용하여 제거 작업의 효율성을 높입니다.
if (removedList.Count > 0)
{
// 제거될 객체들을 먼저 풀에 반환합니다.
foreach (var item in removedList)
{
if (item.IsInPool) item.ReturnToPool();
}
// 그 다음 리스트에서 제거합니다.
var removedItemIds = new HashSet<string>(removedList.Select(i => i.Id));
this.RemoveAll(item => removedItemIds.Contains(item.Id));
}
// [성능 개선] base.AddRange를 사용하여 추적 로직을 우회하고 효율적으로 추가합니다.
if (addedList.Count > 0)
{
base.AddRange(addedList);
}
}
/// <summary>
/// 현재 데이터 객체의 업데이트된 버전을 생성하고 반환합니다. 선택적으로 메모리 할당 최적화를 위해 풀을 사용합니다.
// </summary>
/// <remarks>이 메서드는 현재 데이터 객체와 관련 요소의 깊은 복사를 수행합니다.
/// <paramref name="fromPool"/>이 <see langword="true"/>이면 메서드는 메모리 사용을 최적화하기 위해 객체 풀에서 새 인스턴스를 검색합니다. 그렇지 않으면 새 인스턴스가 직접 생성됩니다. 반환된
/// 객체는 추가, 제거 및 수정을 포함하여 현재 객체의 상태에 대한 업데이트를 반영합니다.
/// 변경 사항은 추적되어 업데이트된 객체의 해당 목록에 적용됩니다.</remarks>
/// <param name="fromPool">객체 풀에서 새 인스턴스를 검색할지 여부를 나타내는 부울 값입니다. <see langword="true"/>
/// 객체 풀을 사용합니다. <see langword="false"/>를 사용하여 새 인스턴스를 직접 생성합니다.</param>
/// <returns>추가, 제거 또는 수정된 목록에서 추적된 모든 변경 사항을 포함하여 현재 객체 데이터의 깊은 복사본을 포함하는 업데이트된 <see cref="IDataObject"/> 인스턴스입니다.
///</returns>
public IDataObject GetUpdatedObject(bool fromPool = true)
{
// 풀에서 새 DataArray 인스턴스를 가져옵니다.
var clone = fromPool ? DataArrayPool.Get() : new DataArray();
clone.FromCapacity(this.Count);
// 배열의 모든 DataObject를 순회하며 각각을 복제합니다.
foreach (var item in this)
{
// DataObject의 Clone 메서드를 호출하여 깊은 복사를 수행하고,
// base.Add를 사용해 추적 로직 없이 직접 추가합니다.
if (item.GetUpdatedObject(fromPool) is DataObject updatedObject)
{
clone.Add(updatedObject);
if (addedList.Contains(item))
{
clone.addedList.Add(updatedObject);
}
else if (removedList.Contains(item))
{
clone.removedList.Add(updatedObject);
}
else if (modifiedList.Contains(item))
{
clone.modifiedList.Add(updatedObject);
}
}
}
return clone;
}
/// <summary>
/// 업데이트 된 속성의 수.
/// </summary>
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
public int UpdatedCount { get => addedList.Count + modifiedList.Count + removedList.Count; }
private bool isUpdateImmediately = false;
/// <summary>
/// 업데이트가 즉시 적용되어야 하는지 여부를 나타냅니다.
/// </summary>
/// <returns></returns>
public bool IsUpdateImmediately
{
get => isUpdateImmediately;
set
{
if (isUpdateImmediately == value) return;
isUpdateImmediately = value;
// 내부 DataObject에도 동일한 값을 설정합니다.
foreach (var item in this)
{
item.IsUpdateImmediately = value;
}
}
}
/// <summary>
/// 컬렉션의 모든 항목을 제거합니다.
/// </summary>
public new void Clear()
{
isUpdateImmediately = false;
// 내부 리스트와 변경 추적 리스트를 모두 비웁니다.
ReturnToDataObjectPool();
base.Clear();
ClearTrackedChanges();
}
/// <summary>
/// 객체를 풀에 반환하기 전에 초기 상태로 리셋합니다.
/// </summary>
public void Reset()
{
Clear();
}
public void ReturnToPool()
{
if (CreatedFromPool)
{
Reset();
}
else
{
// 포함된 DataObject들을 먼저 풀에 반환합니다.
DataArrayPool.Return(this);
}
}
/// <summary>
/// 이 DataArray에 포함된 모든 DataObject를 풀에 반환하고 리스트를 비웁니다.
/// DataArray의 사용이 끝났을 때 호출해야 합니다.
/// </summary>
private void ReturnToDataObjectPool()
{
foreach (var item in this)
{
item.ReturnToPool();
}
}
/// <summary>
/// 변경된 인덱스 목록을 초기화합니다.
/// </summary>
public void ClearTrackedChanges()
{
addedList.Clear();
removedList.Clear();
modifiedList.Clear();
}
/// <summary>
/// 동일한 상태와 값을 가진 현재 데이터 객체의 새 인스턴스를 생성합니다.
/// </summary>
/// <remarks>복제된 객체는 원본 객체와 독립적이므로, 한 객체를 변경해도 다른 객체에는 영향을 미치지 않습니다.
/// </remarks>
/// <param name="fromPool">객체 풀에서 복제할지 여부를 지정합니다. 기본값은 true입니다.</param>
/// <returns>현재 객체의 복사본인 새 <see cref="IDataObject"/> 인스턴스를 반환합니다.</returns>
public IDataObject Clone(bool fromPool = true)
{
return Copy(fromPool);
}
/// <summary>
/// 현재 <see cref="DataArray"/> 인스턴스의 요소 및 관련 상태를 포함한 깊은 복사본을 생성합니다.
///
/// </summary>
/// <remarks>이 메서드는 현재 배열에 있는 모든 <see cref="DataObject"/> 요소의 깊은 복사본을 포함하는 새로운 <see cref="DataArray"/> 인스턴스를 반환합니다.
/// 복사된 인스턴스는 내부 목록에서 추적하는 추가, 제거 또는 수정된 요소를 포함하여 원본 인스턴스의 상태도 복제합니다.
/// </remarks>
/// <param name="fromPool">객체 풀에서 복제할지 여부를 지정합니다. 기본값은 true입니다.</param>
/// <returns>현재 인스턴스의 깊은 복사본인 새로운 <see cref="DataArray"/> 인스턴스를 반환합니다.</returns>
public DataArray Copy(bool fromPool = true)
{
// 풀에서 새 DataArray 인스턴스를 가져옵니다.
DataArray clone;
if (fromPool) clone = DataArrayPool.Get();
else clone = new DataArray();
// 배열의 모든 DataObject를 순회하며 각각을 복제합니다.
foreach (var item in this)
{
// DataObject의 Clone 메서드를 호출하여 깊은 복사를 수행하고,
// base.Add를 사용해 추적 로직 없이 직접 추가합니다.
DataObject clonedItem = item.Copy(fromPool);
clone.Add(clonedItem);
if (addedList.Contains(item))
{
clone.addedList.Add(clonedItem);
}
else if (removedList.Contains(item))
{
clone.removedList.Add(clonedItem);
}
else if (modifiedList.Contains(item))
{
clone.modifiedList.Add(clonedItem);
}
}
return clone;
}
/// <summary>
/// DataArray를 JArray로 변환합니다.
/// </summary>
public JArray ToJArray()
{
JArray array = new JArray();
foreach (var item in this)
{
array.Add(item);
}
return array;
}
/// <summary>
/// DataArray를 JArray로 암시적 변환합니다.
/// </summary>
public static implicit operator JArray(DataArray dataArray)
{
return dataArray.ToJArray();
}
/// <summary>
/// JArray를 DataArray로 암시적 변환합니다.
/// </summary>
public static implicit operator DataArray(JArray jArray)
{
var dataArray = DataArrayPool.Get();
dataArray.FromJArray(jArray);
return dataArray;
}
}
}