2011-11-04 45 views
3

C#程序設計初學者 - 幫助解決非常值得讚賞的基本問題。C#保持對超出範圍的方法所做的更改

我在應用程序中實現了一個通用接口的對象數量。我想創建一個方法,它將接受實現該接口的任何對象集合,對這些對象執行操作,並對原始集合進行轉換。

我看到的行爲是對象在外部調用代碼的方法的範圍內操作,而不是在那裏和呼叫後。我無法弄清楚如何解決這個問題。

這是我一直在測試這個代碼的簡化版本。

接口如下:

public interface IDated 
{ 
    DateTime? Date { get; set; } 
} 

實現該接口的對象的一個​​例子:

public class MyObject : IDated 
{ 
    public string Name { get; set; } 
    public DateTime? Date { get; set; } 
} 

改變的對象的方法:

public class Operator 
{ 
    public void Operate(IEnumerable<IDated> objects) 
    { 
     objects = from o in objects where o.Date.HasValue select o; 
    } 
} 

調用代碼,在最後沒有發生變化:

static void Main(string[] args) 
    { 
     var one = new MyObject("One", null); 
     var two = new MyObject("Two", DateTime.Parse("30-06-06")); 

     var list = new List<MyObject>(); 
     list.Add(one); 
     list.Add(two); 

     new Operator().Operate(list); 

     Console.WriteLine(String.Format("Objects in list = {0}\r\nOperation Succeeded: {1}", list.Count.ToString(), (list.Count == 1).ToString())); 
    } 

輸出爲「2」和「False」。谷歌搜索這個問題,我已經嘗試通過在集合中的「裁判」的說法,如:

public void Operate(ref IEnumerable<IDated> objects) 
    { 
     objects = from o in objects where o.Date.HasValue select o; 
    } 

和:

new Operator().Operate(ref list); 

然而代碼的最後一位有着美麗的充滿活力紅色下劃線,它認爲這個論點是無效的。我無法看到爲什麼?

再次,任何幫助非常感謝。

乾杯,

Tim。

+1

你的'Operate'函數實際上應該*返回*你的新列表。這是獲得新的列表返回到您的通話功能的唯一途徑。 – Gabe

+0

,那麼你有一個List ,但你需要一個列表。因此,你如何在輸出上進行投票?我已經試過這一點,但無法獲得鑄件... – Hanshan

+1

你可以叫你的使用stataments投()上了IEnumerable 如果你有System.Linq的 – TheEvilPenguin

回答

3

您的Operate函數實際上應該是返回您的新列表。這是獲得新的列表返回到您的通話功能的唯一途徑。

注意,這意味着你不能說:

var list = new List<MyObject>(); 
list = new Operator().Operate(list); 

您也需要這樣說:

var list = new List<MyObject>(); 
list.Add(one); 
list.Add(two); 
var output = new Operator().Operate(list); 

或本:

IEnumerable<IDated> list = new List<MyObject> { one, two }; 
list = new Operator().Operate(list); 
+0

唯一的辦法?我不確定這一點。 –

+1

@ValentinKuzub:唯一值得考慮的方法*。 – Gabe

+1

謝謝,Gabe--這是我第一次想到。然而,正如上面評論中提到的,鑄造是一個問題。然而,TheEvilPenguin上面的評論確實解決了這個問題,使其成爲一個有吸引力的解決方案。 – Hanshan

0

你可能要考慮使用擴展方法來構建管道。然後,您的代碼將是這樣的:

public static DatedSetExtensions 
{ 
    public static IEnumerable<IDated> WhereHasDate(this IEnumerable<IDated> input) 
    { 
     return input.Where(x => x.Date.HasValue); 
    } 
} 

... 

list = list.WhereHasDate(); 

不過,請注意IEnumerable的是從列表非常不同。IEnumerable只知道如何遍歷某個集合。它實際上並不存儲它或知道如何操作它。

如果你希望你的原代碼,在給定的收集工作,你應該爲它提供合適的類型,即:收集,如:

public void FilterByHasDate(this Collection<IDated> input) 
    { 
     var itemsToRemove = input.Where(x => x.Date.HasValue).ToArray(); 
     foreach (var itemToRemove in itemsToRemove) 
     { 
      input.Remove(itemToRemove); 
     } 
    } 

編輯我現在想,你希望你的擴展方法返回輸入集合中元素的原始類型。

class Foo {} 
class Bar : Foo {} 

static class FooExtensions 
{ 
    public static IEnumerable<T> WhereSomething<T>(this IEnumerable<T> input) where T: Foo 
    { 
     return input; 
    } 
} 

.... 

List<Bar> bars = new List<Bar>(); 
IEnumerable<Bar> filteredBars = bars.WhereSomething(); 

在這裏,我們有一個運營商正在通過富集,但仍返回酒吧集合,就像我們傳遞給它:您可以輕鬆地使用泛型和類型的限制像管理此。感謝泛型

+0

您的'FilterByHasDate'方法不起作用。首先,您在枚舉集合時通常不能修改集合。另一個問題是任何「ICollection 」都可能是隻讀的。它會一直工作,如果它是'List ',但在那時您可以調用'List.RemoveAll()'。 – Gabe

+0

@加貝 - 感謝您的提示。我試圖將setToRemove移動到內存中,但忘記在示例代碼中添加它。現在會做到這一點。你的第二個陳述是錯誤的。 ICollection 通過契約來操縱集合。你可能與非通用的ICollection混淆,它確實無法修改。 – Polity

+0

我已經嘗試了這一點,它的工作原理(編輯解決方案與泛型)。在我看來,這是一個非常好的解決方案。然而,它遠遠超出了我的想法,所以我已經標記了Gabe的答案,因爲我已經使用了它(雖然添加了Cast <>添加到了混音中)。謝了哥們。 – Hanshan

0

因此,如果您在Operator.Operate方法中執行您的writeline語句,您將獲得預期的結果。我認爲隱式從IList <MyObject>轉換爲IEnumerable <IDated>將創建一個臨時變量,該變量僅在方法調用期間的範圍內。

更改代碼,這(使用簡潔的語法)

static void Main(string[] args) 
     { 
      var one = new MyObject("One", null); 
      var two = new MyObject("Two", DateTime.Parse("06-30-06")); 

      var list = new List<IDated> {one, two}; 

      new Operator().Operate(ref list); 

      Console.WriteLine(String.Format("Objects in list = {0}\r\nOperation Succeeded: {1}", list.Count.ToString(), (list.Count == 1).ToString())); 
      Console.ReadKey(); 
     } 

public class Operator 
{ 
    public void Operate(ref List<IDated> objects) 
    { 
     objects = objects.Where(o => o.Date.HasValue).ToList(); 
     Console.WriteLine(String.Format("Objects in list = {0}\r\nOperation Succeeded: {1}", objects.Count(), objects.Count() == 1)); 
    } 
} 

你得到預期的結果。如果您沒有使用ref關鍵字修飾參數,那麼當該方法超出範圍時,該列表會丟失它的更改。我不確定爲什麼這些都是引用類型,所以我認爲如果沒有涉及到投射,我們會看到主程序中的更改。

+0

hm,如果你從您的操作簽名分配中刪除參考資料不會影響列表之外的列表。當我們調用列表中的Remove()或Clear()並修改原始列表時,可以看到引用類型效果,因爲它不是一個valuetype我們得到了一個實際的列表而不是一個副本,因此我們在inside方法中做的更改正在影響外部列表。 –

0

如果你集中精力尋找使用運營商的解決方案定義爲:

public void Operate(IEnumerable<IDated> objects) 

你顯然不會找到一個方法,因爲不返回任何東西,和IEnumerable參數不能從內部法修改引起任何人外部注意其效果。 (不包括一些聰明的投地List<T>內法,並呼籲Clear()反正)

如果要實際修改輸入參數,你可以把它

public void Operate(List<IDated> objects) 

現在裏面工作,你可以撥打objects.Remove()匹配對象。

而在調用這個void方法後,實際列表將會被修改。

你有路過的時候您嘗試List<T>變量分配給IEnumerable<T>你的方法內List<T>ref IEnumerable<T>是註定的,只是把它作爲ref List<T>裏面的方法,如果你真的喜歡裁判的做法(我不)或指定給通過調用.ToList()致IEnumerable

+0

非常有見地,謝謝。問題:爲什麼我的編譯器推測列表是操作(列表對象)的無效參數,但對操作有效(列表對象)? – Hanshan

+0

@nulliusinverba - 你遇到了協變問題。基本上它是這樣的:儘管'MyObject'「繼承了」來自IDated','List '不會從'List '繼承 - 所以你不能做這個任務。 – Enigmativity

+0

泛型通常不支持您的行編譯所需的協方差。 –

1

創建一個使用yield return的擴展方法(當然,那麼您將返回枚舉而不是修改枚舉本身,這是您在處理時應該做的反正枚舉)。

public static class IDatedExtensions 
{ 
    public static IEnumerable<T> HasDate(IEnumerable<T> objects) 
     where T : IDated 
    { 
     foreach (var obj in objects) 
     { 
      if ((obj != null) && (obj.Date.HasValue)) 
       yield return obj; 
     } 
    } 
} 

用法:

public void Foo() 
{ 
    var list = new List<MyObject>(); 
    list.Add(one); 
    list.Add(two); 

    foreach (var obj in list.HasDate()) 
    { 
     //You should really have this be culture aware since you're working 
     //with DateTime... 
     Console.WriteLine(String.Format(CultureInfo.CurrentCulture, 
      "{0}: {1}", obj.Name, obj.Date.Value)); 
    } 
} 

public void Foo2() 
{ 
    var list = new List<MyObject>(); 
    list.Add(one); 
    list.Add(two); 

    //Use LINQs ToList() to get a new List<T> 
    var newList = list.HasDate().ToList(); 

    ... 
} 

哦,邊注:

Console.WriteLine(string format, params Object[] args)已經格式化字符串你,在它的String.Format是不必要的,除非你計劃供應IFormatProvider以及(在你的情況下,你應該,但你沒有)...

我還應該提到,儘管這與要求的內容並不相同,但我認爲我會將此答案放在那裏,因爲如果您是C#的新手,那麼您最好從右腳開始。不要修改您正在迭代的集合。使用枚舉來枚舉yield返回枚舉中的對象。

+0

爲什麼要經歷產生實例的所有麻煩,當你可以簡單地使用Linq進行過濾並返回查詢? – Polity

+0

他的例子使用'IDated' ...個人,我只實現了一個額外的LINQ方法,這是一個Page方法,其工作方式類似於Skip/Take方法,只是如果您提供了一個超出界限的頁面,返回第一頁/最後一頁,所以你可以做...... foreach(items.Paginate(10,Int32.MaxValue)中的var item){...}'。不過,我認爲OP對於理解如何正確使用擴展方法和枚舉是很好的,因此我回答了這個問題。 –

0

大部分代碼是好的,但這些線路將無法正常工作:

public class Operator 
{ 
    public void Operate(IEnumerable<IDated> objects) 
    { 
     objects = from o in objects where o.Date.HasValue select o; 
    } 
} 

有一些問題,這種方法:

  • 分配到一個正常的非裁判方法的參數沒有按「T改變什麼功能外
  • IEnumerable被設計用於讀取數據,而不是(直接地)修改它

方法的參數是一樣的附加臨時變量

這裏是描述爲何重新分配objects將無法​​正常工作的樣本。這是非常相似的代碼你已經在這裏:

public class SomeClass 
{ 
    public void Something() 
    { 
     List<int> values = new List<int> { 1, 2, 3, 4, 5 }; 
     IEnumerable<int> valuesParam = values; 
     valuesParam = values.Where(i => i < 3); 

     // Will print 1 through 5, not 1 through 2... 
     foreach(int value in values) 
     { 
      Console.WriteLine(value); 
     } 
    } 
} 

的問題是,當你調用一個方法,你讓另一個變量是指您最初的變量。然後當你重新分配給它時,你只是重新分配給那個臨時變量。當這個臨時變量消失時,您的更改也會消失。

IEnumerable的是用於只讀訪問

你會發現,IEnumerable是不是你可以修改。您不能從IEnumerable獲得AddRemove。所以你必須做一些分配才能讓你的Operate方法奏效。

使用ref

你試圖解決這個使用ref,但是這是行不通的,因爲你傳遞的值是一個List<IDated>,不是IEnumerable<IDated>。類型基本上必須匹配。

你可以解決這個問題,但ref是一般使用更痛苦,所以我不會推薦它...

使用返回值

你可以通過重新分配列表的值,而不是解決這個問題,通過傳回的篩選列表作爲返回值。既然你採取IEnumerable<IDated>,我建議你回一個太,並使用對結果ToList()方法把它放回同類型與原始名單。

list = new Operator() 
    .Operate(list) 
    .ToList(); // Note: Important! 

// ... 

public IEnumerable<IDated> Operate(IEnumerable<IDated> objects) 
{ 
    return objects.Where(o => o.Date.HasValue); 
}